fix(synology): resolve pagination offset using correct size before computing page offset

The `size` → `limit` assignment was evaluated after `page * limit`, causing
the offset to be computed using the hardcoded default (100) instead of the
caller-supplied page size. Swapping the two `if` blocks ensures `limit` is
resolved from `size` first so the offset is always `(page-1) * size`.

Adds SYNO-025 and SYNO-026 integration tests that capture the raw Synology
API body and assert `offset` and `limit` are forwarded correctly.
This commit is contained in:
jubnl
2026-04-16 19:49:08 +02:00
parent 272b32b410
commit 465b78411a
2 changed files with 79 additions and 2 deletions
@@ -843,6 +843,83 @@ describe('Synology searchSynologyPhotos date range', () => {
});
});
// ── Search pagination ─────────────────────────────────────────────────────────
describe('Synology search pagination', () => {
it('SYNO-025 — POST /search with { page: 2, size: 50 } sends offset=50 and limit=50 to Synology API', async () => {
const { user } = createUser(testDb);
setSynologyCredentials(testDb, user.id, 'https://synology.example.com', 'admin', 'pass');
let capturedBody: URLSearchParams | null = null;
vi.mocked(safeFetch)
.mockResolvedValueOnce({
// login
ok: true, status: 200,
headers: { get: () => 'application/json' },
json: async () => ({ success: true, data: { sid: 'fake-sid' } }),
body: null,
} as any)
.mockImplementationOnce((_url: string, init?: any) => {
capturedBody = init?.body instanceof URLSearchParams
? init.body
: new URLSearchParams(String(init?.body ?? ''));
return Promise.resolve({
ok: true, status: 200,
headers: { get: () => 'application/json' },
json: async () => ({ success: true, data: { list: [] } }),
body: null,
} as any);
});
const res = await request(app)
.post(`${SYNO}/search`)
.set('Cookie', authCookie(user.id))
.send({ page: 2, size: 50 });
expect(res.status).toBe(200);
expect(capturedBody).not.toBeNull();
// With the fix: limit=50 is resolved first, then offset = (2-1)*50 = 50
expect(capturedBody!.get('offset')).toBe('50');
expect(capturedBody!.get('limit')).toBe('50');
});
it('SYNO-026 — POST /search with { page: 3, size: 25 } sends offset=50 and limit=25 to Synology API', async () => {
const { user } = createUser(testDb);
setSynologyCredentials(testDb, user.id, 'https://synology.example.com', 'admin', 'pass');
let capturedBody: URLSearchParams | null = null;
vi.mocked(safeFetch)
.mockResolvedValueOnce({
ok: true, status: 200,
headers: { get: () => 'application/json' },
json: async () => ({ success: true, data: { sid: 'fake-sid' } }),
body: null,
} as any)
.mockImplementationOnce((_url: string, init?: any) => {
capturedBody = init?.body instanceof URLSearchParams
? init.body
: new URLSearchParams(String(init?.body ?? ''));
return Promise.resolve({
ok: true, status: 200,
headers: { get: () => 'application/json' },
json: async () => ({ success: true, data: { list: [] } }),
body: null,
} as any);
});
const res = await request(app)
.post(`${SYNO}/search`)
.set('Cookie', authCookie(user.id))
.send({ page: 3, size: 25 });
expect(res.status).toBe(200);
expect(capturedBody).not.toBeNull();
// page 3 → page index = 2 (after subtracting 1), offset = 2 * 25 = 50
expect(capturedBody!.get('offset')).toBe('50');
expect(capturedBody!.get('limit')).toBe('25');
});
});
// ── SSRF catch branch in _fetchSynologyJson ────────────────────────────────────
describe('Synology SSRF blocked error handling', () => {