Replace node-fetch v2 with Node 22's built-in fetch API across the entire server.
Add undici as an explicit dependency to provide the dispatcher API needed for
DNS pinning (SSRF rebinding prevention) in ssrfGuard.ts. All seven service files
that used a plain `import fetch from 'node-fetch'` are updated to use the global.
The ssrfGuard safeFetch/createPinnedAgent is rewritten as createPinnedDispatcher
using an undici Agent, with correct handling of the `all: true` lookup callback
required by Node 18+. The collabService dynamic require() and notifications agent
option are updated to use the dispatcher pattern. Test mocks are migrated from
vi.mock('node-fetch') to vi.stubGlobal('fetch'), and streaming test fixtures are
updated to use Web ReadableStream instead of Node Readable.
Fix several bugs in the Synology and Immich photo integrations:
- pipeAsset: guard against setting headers after stream has already started
- _getSynologySession: clear stale SID and re-login when decrypt_api_key returns null
instead of propagating success(null) downstream
- _requestSynologyApi: return retrySession error (not stale session) on retry failure;
also retry on error codes 106 (timeout) and 107 (duplicate login), not only 119
- searchSynologyPhotos: fix incorrect total field type (Synology list_item returns no
total); hasMore correctly uses allItems.length === limit
- _splitPackedSynologyId: validate cache_key format before use; callers return 400
- getImmichCredentials / _getSynologyCredentials: treat null from decrypt_api_key as
a missing-credentials condition rather than casting null to string
- Synology size param: enforce allowlist ['sm', 'm', 'xl'] per API documentation
514 lines
20 KiB
TypeScript
514 lines
20 KiB
TypeScript
/**
|
||
* Immich-specific integration tests (IMMICH-030 – IMMICH-070).
|
||
* Covers status, test-connection, browse, search, asset proxy, access control,
|
||
* and albums — everything NOT covered by the existing immich.test.ts.
|
||
*
|
||
* safeFetch is mocked to return fake Immich API responses based on URL patterns.
|
||
* No real HTTP calls are made.
|
||
*/
|
||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||
import request from 'supertest';
|
||
import type { Application } from 'express';
|
||
|
||
// ── Hoisted DB mock ──────────────────────────────────────────────────────────
|
||
|
||
const { testDb, dbMock } = vi.hoisted(() => {
|
||
const Database = require('better-sqlite3');
|
||
const db = new Database(':memory:');
|
||
db.exec('PRAGMA journal_mode = WAL');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
db.exec('PRAGMA busy_timeout = 5000');
|
||
const mock = {
|
||
db,
|
||
closeDb: () => {},
|
||
reinitialize: () => {},
|
||
getPlaceWithTags: () => null,
|
||
canAccessTrip: (tripId: any, userId: number) =>
|
||
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
|
||
isOwner: (tripId: any, userId: number) =>
|
||
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
|
||
};
|
||
return { testDb: db, dbMock: mock };
|
||
});
|
||
|
||
vi.mock('../../src/db/database', () => dbMock);
|
||
vi.mock('../../src/config', () => ({
|
||
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
|
||
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
|
||
updateJwtSecret: () => {},
|
||
}));
|
||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||
|
||
// ── SSRF guard mock — routes all Immich API calls to fake responses ───────────
|
||
vi.mock('../../src/utils/ssrfGuard', async () => {
|
||
const actual = await vi.importActual<typeof import('../../src/utils/ssrfGuard')>('../../src/utils/ssrfGuard');
|
||
|
||
function makeFakeImmichFetch(url: string, init?: any) {
|
||
const u = typeof url === 'string' ? url : String(url);
|
||
|
||
// /api/users/me — used by status + test-connection
|
||
if (u.includes('/api/users/me')) {
|
||
return Promise.resolve({
|
||
ok: true, status: 200,
|
||
headers: { get: (h: string) => h === 'content-type' ? 'application/json' : null },
|
||
json: () => Promise.resolve({ name: 'Test User', email: 'test@immich.local' }),
|
||
body: null,
|
||
});
|
||
}
|
||
// /api/timeline/buckets — browse
|
||
if (u.includes('/api/timeline/buckets')) {
|
||
return Promise.resolve({
|
||
ok: true, status: 200,
|
||
headers: { get: () => null },
|
||
json: () => Promise.resolve([{ timeBucket: '2024-01-01T00:00:00.000Z', count: 3 }]),
|
||
body: null,
|
||
});
|
||
}
|
||
// /api/search/metadata — search
|
||
if (u.includes('/api/search/metadata')) {
|
||
return Promise.resolve({
|
||
ok: true, status: 200,
|
||
headers: { get: () => null },
|
||
json: () => Promise.resolve({
|
||
assets: {
|
||
items: [
|
||
{ id: 'asset-search-1', fileCreatedAt: '2024-06-01T10:00:00.000Z', exifInfo: { city: 'Paris', country: 'France' } },
|
||
],
|
||
},
|
||
}),
|
||
body: null,
|
||
});
|
||
}
|
||
// /api/assets/:id/thumbnail — thumbnail proxy
|
||
if (u.includes('/thumbnail')) {
|
||
const imageBytes = Buffer.from('fake-thumbnail-data');
|
||
return Promise.resolve({
|
||
ok: true, status: 200,
|
||
headers: { get: (h: string) => h === 'content-type' ? 'image/webp' : null },
|
||
body: new ReadableStream({ start(c) { c.enqueue(imageBytes); c.close(); } }),
|
||
});
|
||
}
|
||
// /api/assets/:id/original — original proxy
|
||
if (u.includes('/original')) {
|
||
const imageBytes = Buffer.from('fake-original-data');
|
||
return Promise.resolve({
|
||
ok: true, status: 200,
|
||
headers: { get: (h: string) => h === 'content-type' ? 'image/jpeg' : null },
|
||
body: new ReadableStream({ start(c) { c.enqueue(imageBytes); c.close(); } }),
|
||
});
|
||
}
|
||
// /api/assets/:id — asset info
|
||
if (/\/api\/assets\/[^/]+$/.test(u)) {
|
||
return Promise.resolve({
|
||
ok: true, status: 200,
|
||
headers: { get: () => null },
|
||
json: () => Promise.resolve({
|
||
id: 'asset-info-1',
|
||
fileCreatedAt: '2024-06-01T10:00:00.000Z',
|
||
originalFileName: 'photo.jpg',
|
||
exifInfo: {
|
||
exifImageWidth: 4032, exifImageHeight: 3024,
|
||
make: 'Apple', model: 'iPhone 15',
|
||
lensModel: null, focalLength: 5.1, fNumber: 1.8,
|
||
exposureTime: '1/500', iso: 100,
|
||
city: 'Paris', state: 'Île-de-France', country: 'France',
|
||
latitude: 48.8566, longitude: 2.3522,
|
||
fileSizeInByte: 2048000,
|
||
},
|
||
}),
|
||
body: null,
|
||
});
|
||
}
|
||
// /api/albums — list albums
|
||
if (/\/api\/albums$/.test(u)) {
|
||
return Promise.resolve({
|
||
ok: true, status: 200,
|
||
headers: { get: () => null },
|
||
json: () => Promise.resolve([
|
||
{ id: 'album-uuid-1', albumName: 'Vacation 2024', assetCount: 42, startDate: '2024-06-01', endDate: '2024-06-14', albumThumbnailAssetId: null },
|
||
]),
|
||
body: null,
|
||
});
|
||
}
|
||
// /api/albums/:id — album detail (for sync)
|
||
if (/\/api\/albums\//.test(u)) {
|
||
return Promise.resolve({
|
||
ok: true, status: 200,
|
||
headers: { get: () => null },
|
||
json: () => Promise.resolve({ assets: [{ id: 'asset-sync-1', type: 'IMAGE' }] }),
|
||
body: null,
|
||
});
|
||
}
|
||
// fallback — unexpected call
|
||
return Promise.reject(new Error(`Unexpected safeFetch call: ${u}`));
|
||
}
|
||
|
||
return {
|
||
...actual,
|
||
checkSsrf: vi.fn().mockImplementation(async (rawUrl: string) => {
|
||
try {
|
||
const url = new URL(rawUrl);
|
||
const h = url.hostname;
|
||
if (h === '127.0.0.1' || h === '::1' || h === 'localhost') {
|
||
return { allowed: false, isPrivate: true, error: 'Loopback not allowed' };
|
||
}
|
||
if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(h)) {
|
||
return { allowed: false, isPrivate: true, error: 'Private IP not allowed' };
|
||
}
|
||
return { allowed: true, isPrivate: false, resolvedIp: '93.184.216.34' };
|
||
} catch {
|
||
return { allowed: false, isPrivate: false, error: 'Invalid URL' };
|
||
}
|
||
}),
|
||
safeFetch: vi.fn().mockImplementation(makeFakeImmichFetch),
|
||
};
|
||
});
|
||
|
||
import { createApp } from '../../src/app';
|
||
import { createTables } from '../../src/db/schema';
|
||
import { runMigrations } from '../../src/db/migrations';
|
||
import { resetTestDb } from '../helpers/test-db';
|
||
import { createUser, createTrip, addTripMember, addTripPhoto, addAlbumLink, setImmichCredentials } from '../helpers/factories';
|
||
import { authCookie } from '../helpers/auth';
|
||
import { loginAttempts, mfaAttempts } from '../../src/routes/auth';
|
||
import { safeFetch } from '../../src/utils/ssrfGuard';
|
||
|
||
const app: Application = createApp();
|
||
|
||
const IMMICH = '/api/integrations/memories/immich';
|
||
|
||
beforeAll(() => {
|
||
createTables(testDb);
|
||
runMigrations(testDb);
|
||
});
|
||
|
||
beforeEach(() => {
|
||
resetTestDb(testDb);
|
||
loginAttempts.clear();
|
||
mfaAttempts.clear();
|
||
});
|
||
|
||
afterAll(() => testDb.close());
|
||
|
||
// ── Connection status ─────────────────────────────────────────────────────────
|
||
|
||
describe('Immich connection status', () => {
|
||
it('IMMICH-030 — GET /status when not configured returns { connected: false }', async () => {
|
||
const { user } = createUser(testDb);
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/status`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(res.body.connected).toBe(false);
|
||
});
|
||
|
||
it('IMMICH-031 — GET /status when configured returns connected + user info', async () => {
|
||
const { user } = createUser(testDb);
|
||
setImmichCredentials(testDb, user.id, 'https://immich.example.com', 'test-api-key');
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/status`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(res.body.connected).toBe(true);
|
||
expect(res.body.user).toMatchObject({ name: 'Test User', email: 'test@immich.local' });
|
||
});
|
||
});
|
||
|
||
// ── Test connection ───────────────────────────────────────────────────────────
|
||
|
||
describe('Immich test connection', () => {
|
||
it('IMMICH-032 — POST /test with missing fields returns { connected: false }', async () => {
|
||
const { user } = createUser(testDb);
|
||
|
||
const res = await request(app)
|
||
.post(`${IMMICH}/test`)
|
||
.set('Cookie', authCookie(user.id))
|
||
.send({ immich_url: 'https://immich.example.com' }); // missing api_key
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(res.body.connected).toBe(false);
|
||
});
|
||
|
||
it('IMMICH-033 — POST /test with valid credentials returns { connected: true }', async () => {
|
||
const { user } = createUser(testDb);
|
||
|
||
const res = await request(app)
|
||
.post(`${IMMICH}/test`)
|
||
.set('Cookie', authCookie(user.id))
|
||
.send({ immich_url: 'https://immich.example.com', immich_api_key: 'valid-key' });
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(res.body.connected).toBe(true);
|
||
expect(res.body.user).toBeDefined();
|
||
});
|
||
});
|
||
|
||
// ── Browse & Search ───────────────────────────────────────────────────────────
|
||
|
||
describe('Immich browse and search', () => {
|
||
it('IMMICH-040 — GET /browse when not configured returns 400', async () => {
|
||
const { user } = createUser(testDb);
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/browse`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(400);
|
||
});
|
||
|
||
it('IMMICH-041 — GET /browse returns timeline buckets', async () => {
|
||
const { user } = createUser(testDb);
|
||
setImmichCredentials(testDb, user.id, 'https://immich.example.com', 'test-api-key');
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/browse`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(Array.isArray(res.body.buckets)).toBe(true);
|
||
expect(res.body.buckets.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('IMMICH-042 — POST /search returns mapped assets', async () => {
|
||
const { user } = createUser(testDb);
|
||
setImmichCredentials(testDb, user.id, 'https://immich.example.com', 'test-api-key');
|
||
|
||
const res = await request(app)
|
||
.post(`${IMMICH}/search`)
|
||
.set('Cookie', authCookie(user.id))
|
||
.send({});
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(Array.isArray(res.body.assets)).toBe(true);
|
||
expect(res.body.assets[0]).toMatchObject({ id: 'asset-search-1', city: 'Paris', country: 'France' });
|
||
});
|
||
|
||
it('IMMICH-043 — POST /search when upstream throws returns 502', async () => {
|
||
const { user } = createUser(testDb);
|
||
setImmichCredentials(testDb, user.id, 'https://immich.example.com', 'test-api-key');
|
||
|
||
vi.mocked(safeFetch).mockRejectedValueOnce(new Error('upstream unreachable'));
|
||
|
||
const res = await request(app)
|
||
.post(`${IMMICH}/search`)
|
||
.set('Cookie', authCookie(user.id))
|
||
.send({});
|
||
|
||
expect(res.status).toBe(502);
|
||
expect(res.body.error).toBeDefined();
|
||
});
|
||
});
|
||
|
||
// ── Asset proxy ───────────────────────────────────────────────────────────────
|
||
|
||
describe('Immich asset proxy', () => {
|
||
it('IMMICH-050 — GET /assets/info returns asset metadata for own photo', async () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
setImmichCredentials(testDb, user.id, 'https://immich.example.com', 'test-api-key');
|
||
addTripPhoto(testDb, trip.id, user.id, 'asset-info-1', 'immich', { shared: false });
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/assets/${trip.id}/asset-info-1/${user.id}/info`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(res.body).toMatchObject({ id: 'asset-info-1', city: 'Paris', country: 'France' });
|
||
});
|
||
|
||
it('IMMICH-051 — GET /assets/info with invalid assetId (special chars) returns 400', async () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
// ID contains characters outside [a-zA-Z0-9_-] → fails isValidAssetId()
|
||
const invalidId = 'asset!@#$%';
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/assets/${trip.id}/${encodeURIComponent(invalidId)}/${user.id}/info`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(400);
|
||
});
|
||
|
||
it('IMMICH-052 — GET /assets/info by non-owner of unshared photo returns 403', async () => {
|
||
const { user: owner } = createUser(testDb);
|
||
const { user: member } = createUser(testDb);
|
||
const trip = createTrip(testDb, owner.id);
|
||
addTripMember(testDb, trip.id, member.id);
|
||
setImmichCredentials(testDb, owner.id, 'https://immich.example.com', 'test-api-key');
|
||
// private photo — shared = false
|
||
addTripPhoto(testDb, trip.id, owner.id, 'asset-private', 'immich', { shared: false });
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/assets/${trip.id}/asset-private/${owner.id}/info`)
|
||
.set('Cookie', authCookie(member.id));
|
||
|
||
expect(res.status).toBe(403);
|
||
});
|
||
|
||
it('IMMICH-053 — GET /assets/info by trip member for shared photo returns 200', async () => {
|
||
const { user: owner } = createUser(testDb);
|
||
const { user: member } = createUser(testDb);
|
||
const trip = createTrip(testDb, owner.id);
|
||
addTripMember(testDb, trip.id, member.id);
|
||
setImmichCredentials(testDb, owner.id, 'https://immich.example.com', 'test-api-key');
|
||
// shared photo
|
||
addTripPhoto(testDb, trip.id, owner.id, 'asset-shared', 'immich', { shared: true });
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/assets/${trip.id}/asset-shared/${owner.id}/info`)
|
||
.set('Cookie', authCookie(member.id));
|
||
|
||
expect(res.status).toBe(200);
|
||
});
|
||
|
||
it('IMMICH-054 — GET /assets/thumbnail for own photo streams image data', async () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
setImmichCredentials(testDb, user.id, 'https://immich.example.com', 'test-api-key');
|
||
addTripPhoto(testDb, trip.id, user.id, 'asset-thumb', 'immich', { shared: false });
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/assets/${trip.id}/asset-thumb/${user.id}/thumbnail`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(res.headers['content-type']).toContain('image/webp');
|
||
expect(res.body).toBeDefined();
|
||
});
|
||
|
||
it('IMMICH-055 — GET /assets/thumbnail for other\'s unshared photo returns 403', async () => {
|
||
const { user: owner } = createUser(testDb);
|
||
const { user: member } = createUser(testDb);
|
||
const trip = createTrip(testDb, owner.id);
|
||
addTripMember(testDb, trip.id, member.id);
|
||
addTripPhoto(testDb, trip.id, owner.id, 'asset-noshare', 'immich', { shared: false });
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/assets/${trip.id}/asset-noshare/${owner.id}/thumbnail`)
|
||
.set('Cookie', authCookie(member.id));
|
||
|
||
expect(res.status).toBe(403);
|
||
});
|
||
|
||
it('IMMICH-056 — GET /assets/original for shared photo streams image data', async () => {
|
||
const { user: owner } = createUser(testDb);
|
||
const { user: member } = createUser(testDb);
|
||
const trip = createTrip(testDb, owner.id);
|
||
addTripMember(testDb, trip.id, member.id);
|
||
setImmichCredentials(testDb, owner.id, 'https://immich.example.com', 'test-api-key');
|
||
addTripPhoto(testDb, trip.id, owner.id, 'asset-orig', 'immich', { shared: true });
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/assets/${trip.id}/asset-orig/${owner.id}/original`)
|
||
.set('Cookie', authCookie(member.id));
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(res.headers['content-type']).toContain('image/jpeg');
|
||
});
|
||
|
||
it('IMMICH-057 — GET /assets/info where trip does not exist returns 403', async () => {
|
||
const { user: owner } = createUser(testDb);
|
||
const { user: member } = createUser(testDb);
|
||
// Insert a shared photo referencing a trip that doesn't exist (FK disabled temporarily)
|
||
testDb.exec('PRAGMA foreign_keys = OFF');
|
||
testDb.prepare(
|
||
'INSERT INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, ?, ?)'
|
||
).run(9999, owner.id, 'asset-notrip', 'immich', 1);
|
||
testDb.exec('PRAGMA foreign_keys = ON');
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/assets/9999/asset-notrip/${owner.id}/info`)
|
||
.set('Cookie', authCookie(member.id));
|
||
|
||
// canAccessUserPhoto: shared photo found, but canAccessTrip(9999) → null → false → 403
|
||
expect(res.status).toBe(403);
|
||
});
|
||
|
||
it('IMMICH-058 — GET /assets/info when upstream returns error propagates status', async () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
setImmichCredentials(testDb, user.id, 'https://immich.example.com', 'test-api-key');
|
||
addTripPhoto(testDb, trip.id, user.id, 'asset-upstream-err', 'immich', { shared: false });
|
||
|
||
vi.mocked(safeFetch).mockResolvedValueOnce({
|
||
ok: false, status: 503,
|
||
headers: { get: () => null } as any,
|
||
json: async () => ({}),
|
||
} as any);
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/assets/${trip.id}/asset-upstream-err/${user.id}/info`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(503);
|
||
expect(res.body.error).toBeDefined();
|
||
});
|
||
});
|
||
|
||
// ── Albums ────────────────────────────────────────────────────────────────────
|
||
|
||
describe('Immich albums', () => {
|
||
it('IMMICH-060 — GET /albums when not configured returns 400', async () => {
|
||
const { user } = createUser(testDb);
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/albums`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(400);
|
||
});
|
||
|
||
it('IMMICH-061 — GET /albums returns album list', async () => {
|
||
const { user } = createUser(testDb);
|
||
setImmichCredentials(testDb, user.id, 'https://immich.example.com', 'test-api-key');
|
||
|
||
const res = await request(app)
|
||
.get(`${IMMICH}/albums`)
|
||
.set('Cookie', authCookie(user.id));
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(Array.isArray(res.body.albums)).toBe(true);
|
||
expect(res.body.albums[0]).toMatchObject({ id: 'album-uuid-1', albumName: 'Vacation 2024' });
|
||
});
|
||
});
|
||
|
||
// ── Auth checks ───────────────────────────────────────────────────────────────
|
||
|
||
describe('Immich auth checks', () => {
|
||
it('IMMICH-070 — GET /status without auth returns 401', async () => {
|
||
expect((await request(app).get(`${IMMICH}/status`)).status).toBe(401);
|
||
});
|
||
|
||
it('IMMICH-070 — POST /test without auth returns 401', async () => {
|
||
expect((await request(app).post(`${IMMICH}/test`)).status).toBe(401);
|
||
});
|
||
|
||
it('IMMICH-070 — GET /browse without auth returns 401', async () => {
|
||
expect((await request(app).get(`${IMMICH}/browse`)).status).toBe(401);
|
||
});
|
||
|
||
it('IMMICH-070 — POST /search without auth returns 401', async () => {
|
||
expect((await request(app).post(`${IMMICH}/search`)).status).toBe(401);
|
||
});
|
||
|
||
it('IMMICH-070 — GET /albums without auth returns 401', async () => {
|
||
expect((await request(app).get(`${IMMICH}/albums`)).status).toBe(401);
|
||
});
|
||
|
||
it('IMMICH-070 — GET /assets/info without auth returns 401', async () => {
|
||
expect((await request(app).get(`${IMMICH}/assets/1/asset-x/1/info`)).status).toBe(401);
|
||
});
|
||
|
||
it('IMMICH-070 — GET /assets/thumbnail without auth returns 401', async () => {
|
||
expect((await request(app).get(`${IMMICH}/assets/1/asset-x/1/thumbnail`)).status).toBe(401);
|
||
});
|
||
|
||
it('IMMICH-070 — GET /assets/original without auth returns 401', async () => {
|
||
expect((await request(app).get(`${IMMICH}/assets/1/asset-x/1/original`)).status).toBe(401);
|
||
});
|
||
});
|