mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	test: reorder tests in asset.e2e-spec.ts (#9714)
* Reorder tests; make tests independent of ordering * use it.each
This commit is contained in:
		
							parent
							
								
									4f21f6a2e1
								
							
						
					
					
						commit
						562c43b6f5
					
				@ -72,7 +72,7 @@ describe('/asset', () => {
 | 
			
		||||
  let stackAssets: AssetFileUploadResponseDto[];
 | 
			
		||||
  let locationAsset: AssetFileUploadResponseDto;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
  const setupTests = async () => {
 | 
			
		||||
    await utils.resetDatabase();
 | 
			
		||||
    admin = await utils.adminSetup({ onboarding: false });
 | 
			
		||||
 | 
			
		||||
@ -155,7 +155,8 @@ describe('/asset', () => {
 | 
			
		||||
      assetId: user1Assets[0].id,
 | 
			
		||||
      personId: person1.id,
 | 
			
		||||
    });
 | 
			
		||||
  }, 30_000);
 | 
			
		||||
  };
 | 
			
		||||
  beforeAll(setupTests, 30_000);
 | 
			
		||||
 | 
			
		||||
  afterAll(() => {
 | 
			
		||||
    utils.disconnectWebsocket(websocket);
 | 
			
		||||
@ -539,359 +540,6 @@ describe('/asset', () => {
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('POST /asset/upload', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).post(`/asset/upload`);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const invalid = [
 | 
			
		||||
      { should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
 | 
			
		||||
      { should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
 | 
			
		||||
      { should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
 | 
			
		||||
      { should: 'require `fileModifiedAt`', dto: { ...makeUploadDto({ omit: 'fileModifiedAt' }) } },
 | 
			
		||||
      { should: 'require `duration`', dto: { ...makeUploadDto({ omit: 'duration' }) } },
 | 
			
		||||
      { should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
 | 
			
		||||
      { should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
 | 
			
		||||
      { should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    for (const { should, dto } of invalid) {
 | 
			
		||||
      it(`should ${should}`, async () => {
 | 
			
		||||
        const { status, body } = await request(app)
 | 
			
		||||
          .post('/asset/upload')
 | 
			
		||||
          .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
			
		||||
          .attach('assetData', makeRandomImage(), 'example.png')
 | 
			
		||||
          .field(dto);
 | 
			
		||||
        expect(status).toBe(400);
 | 
			
		||||
        expect(body).toEqual(errorDto.badRequest());
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const tests = [
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/avif/8bit-sRGB.avif',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '8bit-sRGB.avif',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            description: '',
 | 
			
		||||
            exifImageHeight: 1080,
 | 
			
		||||
            exifImageWidth: 1617,
 | 
			
		||||
            fileSizeInByte: 862_424,
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/jpg/el_torcal_rocks.jpg',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'el_torcal_rocks.jpg',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            dateTimeOriginal: '2012-08-05T11:39:59.000Z',
 | 
			
		||||
            exifImageWidth: 512,
 | 
			
		||||
            exifImageHeight: 341,
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            focalLength: 75,
 | 
			
		||||
            iso: 200,
 | 
			
		||||
            fNumber: 11,
 | 
			
		||||
            exposureTime: '1/160',
 | 
			
		||||
            fileSizeInByte: 53_493,
 | 
			
		||||
            make: 'SONY',
 | 
			
		||||
            model: 'DSLR-A550',
 | 
			
		||||
            orientation: null,
 | 
			
		||||
            description: 'SONY DSC',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/jxl/8bit-sRGB.jxl',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '8bit-sRGB.jxl',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            description: '',
 | 
			
		||||
            exifImageHeight: 1080,
 | 
			
		||||
            exifImageWidth: 1440,
 | 
			
		||||
            fileSizeInByte: 1_780_777,
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/heic/IMG_2682.heic',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'IMG_2682.heic',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2019-03-21T16:04:22.348Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            dateTimeOriginal: '2019-03-21T16:04:22.348Z',
 | 
			
		||||
            exifImageWidth: 4032,
 | 
			
		||||
            exifImageHeight: 3024,
 | 
			
		||||
            latitude: 41.2203,
 | 
			
		||||
            longitude: -96.071_625,
 | 
			
		||||
            make: 'Apple',
 | 
			
		||||
            model: 'iPhone 7',
 | 
			
		||||
            lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
 | 
			
		||||
            fileSizeInByte: 880_703,
 | 
			
		||||
            exposureTime: '1/887',
 | 
			
		||||
            iso: 20,
 | 
			
		||||
            focalLength: 3.99,
 | 
			
		||||
            fNumber: 1.8,
 | 
			
		||||
            timeZone: 'America/Chicago',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/png/density_plot.png',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'density_plot.png',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            exifImageWidth: 800,
 | 
			
		||||
            exifImageHeight: 800,
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            fileSizeInByte: 25_408,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Nikon/D80/glarus.nef',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'glarus.nef',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2010-07-20T17:27:12.000Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'NIKON CORPORATION',
 | 
			
		||||
            model: 'NIKON D80',
 | 
			
		||||
            exposureTime: '1/200',
 | 
			
		||||
            fNumber: 10,
 | 
			
		||||
            focalLength: 18,
 | 
			
		||||
            iso: 100,
 | 
			
		||||
            fileSizeInByte: 9_057_784,
 | 
			
		||||
            dateTimeOriginal: '2010-07-20T17:27:12.000Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Nikon/D700/philadelphia.nef',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'philadelphia.nef',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2016-09-22T22:10:29.060Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'NIKON CORPORATION',
 | 
			
		||||
            model: 'NIKON D700',
 | 
			
		||||
            exposureTime: '1/400',
 | 
			
		||||
            fNumber: 11,
 | 
			
		||||
            focalLength: 85,
 | 
			
		||||
            iso: 200,
 | 
			
		||||
            fileSizeInByte: 15_856_335,
 | 
			
		||||
            dateTimeOriginal: '2016-09-22T22:10:29.060Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
            timeZone: 'UTC-5',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '4_3.rw2',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2018-05-10T08:42:37.842Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'Panasonic',
 | 
			
		||||
            model: 'DMC-GH4',
 | 
			
		||||
            exifImageHeight: 3456,
 | 
			
		||||
            exifImageWidth: 4608,
 | 
			
		||||
            exposureTime: '1/100',
 | 
			
		||||
            fNumber: 3.2,
 | 
			
		||||
            focalLength: 35,
 | 
			
		||||
            iso: 400,
 | 
			
		||||
            fileSizeInByte: 19_587_072,
 | 
			
		||||
            dateTimeOriginal: '2018-05-10T08:42:37.842Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '12bit-compressed-(3_2).arw',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2016-09-27T10:51:44.000Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'SONY',
 | 
			
		||||
            model: 'ILCE-6300',
 | 
			
		||||
            exifImageHeight: 4024,
 | 
			
		||||
            exifImageWidth: 6048,
 | 
			
		||||
            exposureTime: '1/320',
 | 
			
		||||
            fNumber: 8,
 | 
			
		||||
            focalLength: 97,
 | 
			
		||||
            iso: 100,
 | 
			
		||||
            lensModel: 'E PZ 18-105mm F4 G OSS',
 | 
			
		||||
            fileSizeInByte: 25_001_984,
 | 
			
		||||
            dateTimeOriginal: '2016-09-27T10:51:44.000Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '14bit-uncompressed-(3_2).arw',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2016-01-08T15:08:01.000Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'SONY',
 | 
			
		||||
            model: 'ILCE-7M2',
 | 
			
		||||
            exifImageHeight: 4024,
 | 
			
		||||
            exifImageWidth: 6048,
 | 
			
		||||
            exposureTime: '1.3',
 | 
			
		||||
            fNumber: 22,
 | 
			
		||||
            focalLength: 25,
 | 
			
		||||
            iso: 100,
 | 
			
		||||
            lensModel: 'E 25mm F2',
 | 
			
		||||
            fileSizeInByte: 49_512_448,
 | 
			
		||||
            dateTimeOriginal: '2016-01-08T15:08:01.000Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    for (const { input, expected } of tests) {
 | 
			
		||||
      it(`should upload and generate a thumbnail for ${input}`, async () => {
 | 
			
		||||
        const filepath = join(testAssetDir, input);
 | 
			
		||||
        const { id, duplicate } = await utils.createAsset(admin.accessToken, {
 | 
			
		||||
          assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(duplicate).toBe(false);
 | 
			
		||||
 | 
			
		||||
        await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
 | 
			
		||||
 | 
			
		||||
        const asset = await utils.getAssetInfo(admin.accessToken, id);
 | 
			
		||||
 | 
			
		||||
        expect(asset.exifInfo).toBeDefined();
 | 
			
		||||
        expect(asset.exifInfo).toMatchObject(expected.exifInfo);
 | 
			
		||||
        expect(asset).toMatchObject(expected);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    it('should handle a duplicate', async () => {
 | 
			
		||||
      const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
 | 
			
		||||
      const { duplicate } = await utils.createAsset(admin.accessToken, {
 | 
			
		||||
        assetData: {
 | 
			
		||||
          bytes: await readFile(join(testAssetDir, filepath)),
 | 
			
		||||
          filename: basename(filepath),
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(duplicate).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should update the used quota', async () => {
 | 
			
		||||
      const { body, status } = await request(app)
 | 
			
		||||
        .post('/asset/upload')
 | 
			
		||||
        .set('Authorization', `Bearer ${quotaUser.accessToken}`)
 | 
			
		||||
        .field('deviceAssetId', 'example-image')
 | 
			
		||||
        .field('deviceId', 'e2e')
 | 
			
		||||
        .field('fileCreatedAt', new Date().toISOString())
 | 
			
		||||
        .field('fileModifiedAt', new Date().toISOString())
 | 
			
		||||
        .attach('assetData', makeRandomImage(), 'example.jpg');
 | 
			
		||||
 | 
			
		||||
      expect(body).toEqual({ id: expect.any(String), duplicate: false });
 | 
			
		||||
      expect(status).toBe(201);
 | 
			
		||||
 | 
			
		||||
      const user = await getMyUserInfo({ headers: asBearerAuth(quotaUser.accessToken) });
 | 
			
		||||
 | 
			
		||||
      expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not upload an asset if it would exceed the quota', async () => {
 | 
			
		||||
      const { body, status } = await request(app)
 | 
			
		||||
        .post('/asset/upload')
 | 
			
		||||
        .set('Authorization', `Bearer ${quotaUser.accessToken}`)
 | 
			
		||||
        .field('deviceAssetId', 'example-image')
 | 
			
		||||
        .field('deviceId', 'e2e')
 | 
			
		||||
        .field('fileCreatedAt', new Date().toISOString())
 | 
			
		||||
        .field('fileModifiedAt', new Date().toISOString())
 | 
			
		||||
        .attach('assetData', randomBytes(2014), 'example.jpg');
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest('Quota has been exceeded!'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // These hashes were created by copying the image files to a Samsung phone,
 | 
			
		||||
    // exporting the video from Samsung's stock Gallery app, and hashing them locally.
 | 
			
		||||
    // This ensures that immich+exiftool are extracting the videos the same way Samsung does.
 | 
			
		||||
    // DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
 | 
			
		||||
    // into the test here.
 | 
			
		||||
    const motionTests = [
 | 
			
		||||
      {
 | 
			
		||||
        filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
 | 
			
		||||
        checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        filepath: 'formats/motionphoto/Samsung One UI 6.jpg',
 | 
			
		||||
        checksum: 'lT9Uviw/FFJYCjfIxAGPTjzAmmw=',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        filepath: 'formats/motionphoto/Samsung One UI 6.heic',
 | 
			
		||||
        checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    for (const { filepath, checksum } of motionTests) {
 | 
			
		||||
      it(`should extract motionphoto video from ${filepath}`, async () => {
 | 
			
		||||
        const response = await utils.createAsset(admin.accessToken, {
 | 
			
		||||
          assetData: {
 | 
			
		||||
            bytes: await readFile(join(testAssetDir, filepath)),
 | 
			
		||||
            filename: basename(filepath),
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
 | 
			
		||||
 | 
			
		||||
        expect(response.duplicate).toBe(false);
 | 
			
		||||
 | 
			
		||||
        const asset = await utils.getAssetInfo(admin.accessToken, response.id);
 | 
			
		||||
        expect(asset.livePhotoVideoId).toBeDefined();
 | 
			
		||||
 | 
			
		||||
        const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
 | 
			
		||||
        expect(video.checksum).toStrictEqual(checksum);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('GET /asset/thumbnail/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
 | 
			
		||||
@ -963,6 +611,31 @@ describe('/asset', () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('GET /asset/map-marker', () => {
 | 
			
		||||
    beforeAll(async () => {
 | 
			
		||||
      const files = [
 | 
			
		||||
        'formats/avif/8bit-sRGB.avif',
 | 
			
		||||
        'formats/jpg/el_torcal_rocks.jpg',
 | 
			
		||||
        'formats/jxl/8bit-sRGB.jxl',
 | 
			
		||||
        'formats/heic/IMG_2682.heic',
 | 
			
		||||
        'formats/png/density_plot.png',
 | 
			
		||||
        'formats/raw/Nikon/D80/glarus.nef',
 | 
			
		||||
        'formats/raw/Nikon/D700/philadelphia.nef',
 | 
			
		||||
        'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
 | 
			
		||||
        'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
 | 
			
		||||
        'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
 | 
			
		||||
      ];
 | 
			
		||||
      utils.resetEvents();
 | 
			
		||||
      const uploadFile = async (input: string) => {
 | 
			
		||||
        const filepath = join(testAssetDir, input);
 | 
			
		||||
        const { id } = await utils.createAsset(admin.accessToken, {
 | 
			
		||||
          assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
 | 
			
		||||
        });
 | 
			
		||||
        await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
 | 
			
		||||
      };
 | 
			
		||||
      const uploads = files.map((f) => uploadFile(f));
 | 
			
		||||
      await Promise.all(uploads);
 | 
			
		||||
    }, 30_000);
 | 
			
		||||
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get('/asset/map-marker');
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
@ -1193,4 +866,346 @@ describe('/asset', () => {
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  describe('POST /asset/upload', () => {
 | 
			
		||||
    beforeAll(setupTests, 30_000);
 | 
			
		||||
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).post(`/asset/upload`);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it.each([
 | 
			
		||||
      { should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
 | 
			
		||||
      { should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
 | 
			
		||||
      { should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
 | 
			
		||||
      { should: 'require `fileModifiedAt`', dto: { ...makeUploadDto({ omit: 'fileModifiedAt' }) } },
 | 
			
		||||
      { should: 'require `duration`', dto: { ...makeUploadDto({ omit: 'duration' }) } },
 | 
			
		||||
      { should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
 | 
			
		||||
      { should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
 | 
			
		||||
      { should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
 | 
			
		||||
    ])('should $should', async ({ dto }) => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .post('/asset/upload')
 | 
			
		||||
        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
			
		||||
        .attach('assetData', makeRandomImage(), 'example.png')
 | 
			
		||||
        .field(dto);
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it.each([
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/avif/8bit-sRGB.avif',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '8bit-sRGB.avif',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            description: '',
 | 
			
		||||
            exifImageHeight: 1080,
 | 
			
		||||
            exifImageWidth: 1617,
 | 
			
		||||
            fileSizeInByte: 862_424,
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/jpg/el_torcal_rocks.jpg',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'el_torcal_rocks.jpg',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            dateTimeOriginal: '2012-08-05T11:39:59.000Z',
 | 
			
		||||
            exifImageWidth: 512,
 | 
			
		||||
            exifImageHeight: 341,
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            focalLength: 75,
 | 
			
		||||
            iso: 200,
 | 
			
		||||
            fNumber: 11,
 | 
			
		||||
            exposureTime: '1/160',
 | 
			
		||||
            fileSizeInByte: 53_493,
 | 
			
		||||
            make: 'SONY',
 | 
			
		||||
            model: 'DSLR-A550',
 | 
			
		||||
            orientation: null,
 | 
			
		||||
            description: 'SONY DSC',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/jxl/8bit-sRGB.jxl',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '8bit-sRGB.jxl',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            description: '',
 | 
			
		||||
            exifImageHeight: 1080,
 | 
			
		||||
            exifImageWidth: 1440,
 | 
			
		||||
            fileSizeInByte: 1_780_777,
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/heic/IMG_2682.heic',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'IMG_2682.heic',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2019-03-21T16:04:22.348Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            dateTimeOriginal: '2019-03-21T16:04:22.348Z',
 | 
			
		||||
            exifImageWidth: 4032,
 | 
			
		||||
            exifImageHeight: 3024,
 | 
			
		||||
            latitude: 41.2203,
 | 
			
		||||
            longitude: -96.071_625,
 | 
			
		||||
            make: 'Apple',
 | 
			
		||||
            model: 'iPhone 7',
 | 
			
		||||
            lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
 | 
			
		||||
            fileSizeInByte: 880_703,
 | 
			
		||||
            exposureTime: '1/887',
 | 
			
		||||
            iso: 20,
 | 
			
		||||
            focalLength: 3.99,
 | 
			
		||||
            fNumber: 1.8,
 | 
			
		||||
            timeZone: 'America/Chicago',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/png/density_plot.png',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'density_plot.png',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            exifImageWidth: 800,
 | 
			
		||||
            exifImageHeight: 800,
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            fileSizeInByte: 25_408,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Nikon/D80/glarus.nef',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'glarus.nef',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2010-07-20T17:27:12.000Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'NIKON CORPORATION',
 | 
			
		||||
            model: 'NIKON D80',
 | 
			
		||||
            exposureTime: '1/200',
 | 
			
		||||
            fNumber: 10,
 | 
			
		||||
            focalLength: 18,
 | 
			
		||||
            iso: 100,
 | 
			
		||||
            fileSizeInByte: 9_057_784,
 | 
			
		||||
            dateTimeOriginal: '2010-07-20T17:27:12.000Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Nikon/D700/philadelphia.nef',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: 'philadelphia.nef',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2016-09-22T22:10:29.060Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'NIKON CORPORATION',
 | 
			
		||||
            model: 'NIKON D700',
 | 
			
		||||
            exposureTime: '1/400',
 | 
			
		||||
            fNumber: 11,
 | 
			
		||||
            focalLength: 85,
 | 
			
		||||
            iso: 200,
 | 
			
		||||
            fileSizeInByte: 15_856_335,
 | 
			
		||||
            dateTimeOriginal: '2016-09-22T22:10:29.060Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
            timeZone: 'UTC-5',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '4_3.rw2',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2018-05-10T08:42:37.842Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'Panasonic',
 | 
			
		||||
            model: 'DMC-GH4',
 | 
			
		||||
            exifImageHeight: 3456,
 | 
			
		||||
            exifImageWidth: 4608,
 | 
			
		||||
            exposureTime: '1/100',
 | 
			
		||||
            fNumber: 3.2,
 | 
			
		||||
            focalLength: 35,
 | 
			
		||||
            iso: 400,
 | 
			
		||||
            fileSizeInByte: 19_587_072,
 | 
			
		||||
            dateTimeOriginal: '2018-05-10T08:42:37.842Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '12bit-compressed-(3_2).arw',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2016-09-27T10:51:44.000Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'SONY',
 | 
			
		||||
            model: 'ILCE-6300',
 | 
			
		||||
            exifImageHeight: 4024,
 | 
			
		||||
            exifImageWidth: 6048,
 | 
			
		||||
            exposureTime: '1/320',
 | 
			
		||||
            fNumber: 8,
 | 
			
		||||
            focalLength: 97,
 | 
			
		||||
            iso: 100,
 | 
			
		||||
            lensModel: 'E PZ 18-105mm F4 G OSS',
 | 
			
		||||
            fileSizeInByte: 25_001_984,
 | 
			
		||||
            dateTimeOriginal: '2016-09-27T10:51:44.000Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        input: 'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
 | 
			
		||||
        expected: {
 | 
			
		||||
          type: AssetTypeEnum.Image,
 | 
			
		||||
          originalFileName: '14bit-uncompressed-(3_2).arw',
 | 
			
		||||
          resized: true,
 | 
			
		||||
          fileCreatedAt: '2016-01-08T15:08:01.000Z',
 | 
			
		||||
          exifInfo: {
 | 
			
		||||
            make: 'SONY',
 | 
			
		||||
            model: 'ILCE-7M2',
 | 
			
		||||
            exifImageHeight: 4024,
 | 
			
		||||
            exifImageWidth: 6048,
 | 
			
		||||
            exposureTime: '1.3',
 | 
			
		||||
            fNumber: 22,
 | 
			
		||||
            focalLength: 25,
 | 
			
		||||
            iso: 100,
 | 
			
		||||
            lensModel: 'E 25mm F2',
 | 
			
		||||
            fileSizeInByte: 49_512_448,
 | 
			
		||||
            dateTimeOriginal: '2016-01-08T15:08:01.000Z',
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
            orientation: '1',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ])(`should upload and generate a thumbnail for $input`, async ({ input, expected }) => {
 | 
			
		||||
      const filepath = join(testAssetDir, input);
 | 
			
		||||
      const { id, duplicate } = await utils.createAsset(admin.accessToken, {
 | 
			
		||||
        assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(duplicate).toBe(false);
 | 
			
		||||
 | 
			
		||||
      await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
 | 
			
		||||
 | 
			
		||||
      const asset = await utils.getAssetInfo(admin.accessToken, id);
 | 
			
		||||
 | 
			
		||||
      expect(asset.exifInfo).toBeDefined();
 | 
			
		||||
      expect(asset.exifInfo).toMatchObject(expected.exifInfo);
 | 
			
		||||
      expect(asset).toMatchObject(expected);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle a duplicate', async () => {
 | 
			
		||||
      const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
 | 
			
		||||
      const { duplicate } = await utils.createAsset(admin.accessToken, {
 | 
			
		||||
        assetData: {
 | 
			
		||||
          bytes: await readFile(join(testAssetDir, filepath)),
 | 
			
		||||
          filename: basename(filepath),
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(duplicate).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should update the used quota', async () => {
 | 
			
		||||
      const { body, status } = await request(app)
 | 
			
		||||
        .post('/asset/upload')
 | 
			
		||||
        .set('Authorization', `Bearer ${quotaUser.accessToken}`)
 | 
			
		||||
        .field('deviceAssetId', 'example-image')
 | 
			
		||||
        .field('deviceId', 'e2e')
 | 
			
		||||
        .field('fileCreatedAt', new Date().toISOString())
 | 
			
		||||
        .field('fileModifiedAt', new Date().toISOString())
 | 
			
		||||
        .attach('assetData', makeRandomImage(), 'example.jpg');
 | 
			
		||||
 | 
			
		||||
      expect(body).toEqual({ id: expect.any(String), duplicate: false });
 | 
			
		||||
      expect(status).toBe(201);
 | 
			
		||||
 | 
			
		||||
      const user = await getMyUserInfo({ headers: asBearerAuth(quotaUser.accessToken) });
 | 
			
		||||
 | 
			
		||||
      expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not upload an asset if it would exceed the quota', async () => {
 | 
			
		||||
      const { body, status } = await request(app)
 | 
			
		||||
        .post('/asset/upload')
 | 
			
		||||
        .set('Authorization', `Bearer ${quotaUser.accessToken}`)
 | 
			
		||||
        .field('deviceAssetId', 'example-image')
 | 
			
		||||
        .field('deviceId', 'e2e')
 | 
			
		||||
        .field('fileCreatedAt', new Date().toISOString())
 | 
			
		||||
        .field('fileModifiedAt', new Date().toISOString())
 | 
			
		||||
        .attach('assetData', randomBytes(2014), 'example.jpg');
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest('Quota has been exceeded!'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // These hashes were created by copying the image files to a Samsung phone,
 | 
			
		||||
    // exporting the video from Samsung's stock Gallery app, and hashing them locally.
 | 
			
		||||
    // This ensures that immich+exiftool are extracting the videos the same way Samsung does.
 | 
			
		||||
    // DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
 | 
			
		||||
    // into the test here.
 | 
			
		||||
    it.each([
 | 
			
		||||
      {
 | 
			
		||||
        filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
 | 
			
		||||
        checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        filepath: 'formats/motionphoto/Samsung One UI 6.jpg',
 | 
			
		||||
        checksum: 'lT9Uviw/FFJYCjfIxAGPTjzAmmw=',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        filepath: 'formats/motionphoto/Samsung One UI 6.heic',
 | 
			
		||||
        checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
 | 
			
		||||
      },
 | 
			
		||||
    ])(`should extract motionphoto video from $filepath`, async ({ filepath, checksum }) => {
 | 
			
		||||
      const response = await utils.createAsset(admin.accessToken, {
 | 
			
		||||
        assetData: {
 | 
			
		||||
          bytes: await readFile(join(testAssetDir, filepath)),
 | 
			
		||||
          filename: basename(filepath),
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
 | 
			
		||||
 | 
			
		||||
      expect(response.duplicate).toBe(false);
 | 
			
		||||
 | 
			
		||||
      const asset = await utils.getAssetInfo(admin.accessToken, response.id);
 | 
			
		||||
      expect(asset.livePhotoVideoId).toBeDefined();
 | 
			
		||||
 | 
			
		||||
      const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
 | 
			
		||||
      expect(video.checksum).toStrictEqual(checksum);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user