release: v0.20.0
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { NextRequest } from 'next/server';
|
||||
import {
|
||||
LAT,
|
||||
cameraHeic,
|
||||
cameraJpeg,
|
||||
chunked,
|
||||
contains,
|
||||
latin1,
|
||||
} from '@/lib/__tests__/fixtures/location';
|
||||
|
||||
/**
|
||||
* The archive route streams originals as a ZIP for either the whole album
|
||||
@@ -430,3 +438,103 @@ describe('concurrent archives', () => {
|
||||
mockClientIp.mockReturnValue('127.0.0.1');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The ZIP is stored uncompressed, so each entry's bytes appear in it as they
|
||||
* are: what the scrubber left is exactly what a visitor unpacks.
|
||||
*/
|
||||
describe('location metadata', () => {
|
||||
it('leaves GPS out of every original in the archive and keeps camera data', async () => {
|
||||
const jpeg = cameraJpeg(false).file;
|
||||
const heic = cameraHeic({ le: true }).file;
|
||||
mockStream.mockImplementation((id: string) =>
|
||||
Promise.resolve({
|
||||
stream: chunked(id === 'asset-1' ? jpeg : heic, 512),
|
||||
contentType: id === 'asset-1' ? 'image/jpeg' : 'image/heic',
|
||||
contentLength: null,
|
||||
}),
|
||||
);
|
||||
|
||||
const zip = new Uint8Array(await (await GET(getReq(), params)).arrayBuffer());
|
||||
expect(contains(zip, LAT.value(false))).toBe(false);
|
||||
expect(contains(zip, LAT.value(true))).toBe(false);
|
||||
const text = latin1(zip);
|
||||
expect(text).not.toMatch(/exif:GPS/);
|
||||
expect(text).not.toContain('52.520095');
|
||||
expect(text.split('Canon EOS R5').length - 1).toBeGreaterThanOrEqual(2);
|
||||
expect(text).toContain('photoshop:City="Berlin"');
|
||||
});
|
||||
|
||||
it('leaves out an original whose metadata cannot be located, and goes on', async () => {
|
||||
const broken = new Uint8Array([0xff, 0xd8, 0xff, 0xe1, 0x40, 0x00, 0x45, 0x78]);
|
||||
mockStream.mockImplementation((id: string) =>
|
||||
Promise.resolve(
|
||||
id === 'asset-1'
|
||||
? { stream: chunked(broken, 8), contentType: 'image/jpeg', contentLength: null }
|
||||
: originStream('second-original'),
|
||||
),
|
||||
);
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const zip = latin1(new Uint8Array(await (await GET(getReq(), params)).arrayBuffer()));
|
||||
// Visible in the server log: which asset, which album, and how many.
|
||||
const logged = warn.mock.calls.map((args) => String(args[0]));
|
||||
expect(logged.some((l) => l.includes('asset-1') && l.includes('Test Album'))).toBe(true);
|
||||
expect(logged.some((l) => l.includes('missing 1 of 2'))).toBe(true);
|
||||
warn.mockRestore();
|
||||
expect(zip).not.toContain('photo-1.jpg');
|
||||
expect(zip).toContain('photo-2.jpg');
|
||||
expect(zip).toContain('second-original');
|
||||
});
|
||||
|
||||
it('puts a photo edited in Immich in as edited, renamed, and without GPS (#831)', async () => {
|
||||
mockGetAlbum.mockResolvedValue({
|
||||
...ALBUM,
|
||||
assets: [
|
||||
{ id: 'asset-1', type: 'IMAGE', originalFileName: 'IMG_9262.HEIC', isEdited: true },
|
||||
{ id: 'asset-2', type: 'IMAGE', originalFileName: 'photo-2.jpg' },
|
||||
],
|
||||
});
|
||||
const jpeg = cameraJpeg(true).file;
|
||||
mockStream.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
stream: chunked(jpeg, 512),
|
||||
contentType: 'image/jpeg',
|
||||
contentLength: null,
|
||||
}),
|
||||
);
|
||||
|
||||
const zip = new Uint8Array(await (await GET(getReq(), params)).arrayBuffer());
|
||||
expect(mockStream).toHaveBeenCalledWith('asset-1', 'original', true);
|
||||
expect(mockStream).toHaveBeenCalledWith('asset-2', 'original', false);
|
||||
const text = latin1(zip);
|
||||
expect(text).toContain('IMG_9262.jpg');
|
||||
expect(text).not.toContain('IMG_9262.HEIC');
|
||||
expect(contains(zip, LAT.value(true))).toBe(false);
|
||||
});
|
||||
|
||||
it('leaves out an edited rendition the scrubber would pass through (review of #832)', async () => {
|
||||
mockGetAlbum.mockResolvedValue({
|
||||
...ALBUM,
|
||||
assets: [
|
||||
{ id: 'asset-1', type: 'IMAGE', originalFileName: 'IMG_9262.HEIC', isEdited: true },
|
||||
{ id: 'asset-2', type: 'IMAGE', originalFileName: 'photo-2.jpg' },
|
||||
],
|
||||
});
|
||||
const webp = new TextEncoder().encode('RIFF\x10\x00\x00\x00WEBPVP8 edited-webp');
|
||||
mockStream.mockImplementation((id: string) =>
|
||||
Promise.resolve(
|
||||
id === 'asset-1'
|
||||
? { stream: chunked(webp, 64), contentType: 'image/webp', contentLength: null }
|
||||
: originStream('second-original'),
|
||||
),
|
||||
);
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const zip = latin1(new Uint8Array(await (await GET(getReq(), params)).arrayBuffer()));
|
||||
const logged = warn.mock.calls.map((args) => String(args[0]));
|
||||
expect(logged.some((l) => l.includes('edited asset asset-1'))).toBe(true);
|
||||
warn.mockRestore();
|
||||
expect(zip).not.toContain('edited-webp');
|
||||
expect(zip).not.toContain('IMG_9262');
|
||||
expect(zip).toContain('second-original');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user