test(server): fix testing gaps and failing tests

1. Add DELETE /api/sync/data route tests to sync.routes.spec.ts:
   - Test successful deletion returns { success: true }
   - Test 401 without authorization
   - Test uploading new data works after reset

2. Fix passkey.spec.ts failures (4 tests):
   - Add missing passkey.findUnique mock for credential lookup
   - Update test expectations for discoverable credentials
     (no allowCredentials - implementation changed)

3. Fix password-reset-api.spec.ts failures (12 tests):
   - Exclude from vitest - tests routes that don't exist
   - Server uses passkey/magic link auth, not password auth

All 412 tests now pass.
This commit is contained in:
Johannes Millan 2026-01-05 17:39:17 +01:00
parent 001d89d58e
commit f0f536671b
3 changed files with 127 additions and 6 deletions

View file

@ -25,6 +25,7 @@ vi.mock('../src/db', () => {
create: vi.fn(),
update: vi.fn(),
deleteMany: vi.fn(),
findUnique: vi.fn(),
},
$transaction: vi.fn(),
};
@ -86,6 +87,7 @@ describe('Passkey Authentication', () => {
create: Mock;
update: Mock;
deleteMany: Mock;
findUnique: Mock;
};
$transaction: Mock;
};
@ -281,14 +283,12 @@ describe('Passkey Authentication', () => {
expect(options).toBeDefined();
expect(options.challenge).toBe(testChallenge);
// Implementation uses discoverable credentials (no allowCredentials)
// to let browser show all available passkeys for this RP
expect(mockGenerateAuthentication).toHaveBeenCalledWith(
expect.objectContaining({
allowCredentials: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
transports: ['internal'],
}),
]),
rpID: 'localhost',
userVerification: 'preferred',
}),
);
});
@ -311,6 +311,16 @@ describe('Passkey Authentication', () => {
passkeys: [mockPasskey],
});
// Mock passkey lookup by credential ID (new implementation uses discoverable credentials)
mockPrisma.passkey.findUnique.mockResolvedValue({
...mockPasskey,
user: {
id: 1,
email: testEmail,
isVerified: 1,
},
});
mockVerifyAuthentication.mockResolvedValue({
verified: true,
authenticationInfo: {
@ -356,6 +366,16 @@ describe('Passkey Authentication', () => {
passkeys: [mockPasskey],
});
// Mock passkey lookup - user is unverified
mockPrisma.passkey.findUnique.mockResolvedValue({
...mockPasskey,
user: {
id: 1,
email: testEmail,
isVerified: 0,
},
});
// Generate options first
await generateAuthenticationOptions(testEmail);
@ -384,6 +404,9 @@ describe('Passkey Authentication', () => {
passkeys: [mockPasskey],
});
// Mock passkey lookup returns null (credential not found)
mockPrisma.passkey.findUnique.mockResolvedValue(null);
// Generate options first
await generateAuthenticationOptions(testEmail);

View file

@ -1440,4 +1440,100 @@ describe('Restore Points API', () => {
expect(retryResponse.json().errorCode).toBe('SYNC_IMPORT_EXISTS');
});
});
describe('DELETE /api/sync/data - Reset Account (Delete All User Data)', () => {
it('should delete all user data and return success', async () => {
// First upload some operations
await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: { authorization: `Bearer ${authToken}` },
payload: {
ops: [
createOp(clientId, { entityId: 'task-1' }),
createOp(clientId, { entityId: 'task-2' }),
],
clientId,
},
});
// Verify ops exist
const opsBefore = await app.inject({
method: 'GET',
url: '/api/sync/ops?sinceSeq=0',
headers: { authorization: `Bearer ${authToken}` },
});
expect(opsBefore.json().ops.length).toBe(2);
// Delete all user data
const deleteResponse = await app.inject({
method: 'DELETE',
url: '/api/sync/data',
headers: { authorization: `Bearer ${authToken}` },
});
expect(deleteResponse.statusCode).toBe(200);
expect(deleteResponse.json()).toEqual({ success: true });
// Verify ops are gone
const opsAfter = await app.inject({
method: 'GET',
url: '/api/sync/ops?sinceSeq=0',
headers: { authorization: `Bearer ${authToken}` },
});
expect(opsAfter.json().ops.length).toBe(0);
});
it('should return 401 without authorization', async () => {
const response = await app.inject({
method: 'DELETE',
url: '/api/sync/data',
});
expect(response.statusCode).toBe(401);
});
it('should allow uploading new data after reset', async () => {
// Upload initial data
await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: { authorization: `Bearer ${authToken}` },
payload: {
ops: [createOp(clientId, { entityId: 'old-task' })],
clientId,
},
});
// Delete all data
await app.inject({
method: 'DELETE',
url: '/api/sync/data',
headers: { authorization: `Bearer ${authToken}` },
});
// Upload new data after reset
const uploadResponse = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: { authorization: `Bearer ${authToken}` },
payload: {
ops: [createOp(clientId, { entityId: 'new-task' })],
clientId,
},
});
expect(uploadResponse.statusCode).toBe(200);
expect(uploadResponse.json().results[0].accepted).toBe(true);
// Verify only new data exists
const ops = await app.inject({
method: 'GET',
url: '/api/sync/ops?sinceSeq=0',
headers: { authorization: `Bearer ${authToken}` },
});
expect(ops.json().ops.length).toBe(1);
expect(ops.json().ops[0].entityId).toBe('new-task');
});
});
});

View file

@ -21,6 +21,8 @@ export default defineConfig({
'tests/snapshot-skip-optimization.spec.ts',
'tests/integration/multi-client-sync.integration.spec.ts',
'tests/integration/snapshot-skip-optimization.integration.spec.ts',
// Tests password reset routes that don't exist - server uses passkey/magic link auth
'tests/password-reset-api.spec.ts',
],
},
});