feat(phase3): phase 7 - integration & polish
Fix all build/type/lint errors, write 46 new tests (222 total across
20 files), regenerate Prisma client, update seed with user preferences.
Fix SvelteSet usage, add {#each} keys, clean unused imports.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('$lib/server/services/appService.js', () => ({
|
||||
create: vi.fn()
|
||||
}));
|
||||
|
||||
import * as appService from '$lib/server/services/appService.js';
|
||||
import { POST } from '../+server.js';
|
||||
|
||||
const mockCreate = appService.create as ReturnType<typeof vi.fn>;
|
||||
|
||||
function createMockEvent(
|
||||
overrides: {
|
||||
user?: { id: string; role: string } | null;
|
||||
body?: unknown;
|
||||
jsonThrows?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const { user = { id: 'u1', role: 'user' }, body = {}, jsonThrows = false } = overrides;
|
||||
|
||||
return {
|
||||
locals: { user },
|
||||
request: {
|
||||
json: jsonThrows
|
||||
? vi.fn().mockRejectedValue(new Error('Invalid JSON'))
|
||||
: vi.fn().mockResolvedValue(body)
|
||||
}
|
||||
} as unknown as Parameters<typeof POST>[0];
|
||||
}
|
||||
|
||||
describe('Quick-Add API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('POST /api/apps/quick-add', () => {
|
||||
it('creates app with valid URL and name', async () => {
|
||||
const createdApp = {
|
||||
id: 'app1',
|
||||
name: 'My App',
|
||||
url: 'https://myapp.example.com',
|
||||
icon: 'https://myapp.example.com/favicon.ico',
|
||||
iconType: 'url'
|
||||
};
|
||||
mockCreate.mockResolvedValue(createdApp);
|
||||
|
||||
const response = await POST(
|
||||
createMockEvent({
|
||||
body: { url: 'https://myapp.example.com', name: 'My App' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.data).toEqual(createdApp);
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'My App',
|
||||
url: 'https://myapp.example.com',
|
||||
icon: 'https://myapp.example.com/favicon.ico',
|
||||
iconType: 'url',
|
||||
healthcheckEnabled: true,
|
||||
createdById: 'u1'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('derives favicon URL from app URL', async () => {
|
||||
mockCreate.mockResolvedValue({ id: 'app2' });
|
||||
|
||||
await POST(
|
||||
createMockEvent({
|
||||
body: { url: 'https://git.example.com/repos', name: 'Gitea' }
|
||||
})
|
||||
);
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
icon: 'https://git.example.com/favicon.ico'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid URL', async () => {
|
||||
const response = await POST(
|
||||
createMockEvent({
|
||||
body: { url: 'not-a-url', name: 'Bad App' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing name', async () => {
|
||||
const response = await POST(
|
||||
createMockEvent({
|
||||
body: { url: 'https://example.com' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects non-http URLs', async () => {
|
||||
const response = await POST(
|
||||
createMockEvent({
|
||||
body: { url: 'ftp://files.example.com', name: 'FTP Server' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('returns 400 for invalid JSON body', async () => {
|
||||
const response = await POST(createMockEvent({ jsonThrows: true }));
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('returns 500 when service throws', async () => {
|
||||
mockCreate.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
const response = await POST(
|
||||
createMockEvent({
|
||||
body: { url: 'https://example.com', name: 'Failing App' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('redirects when not authenticated', async () => {
|
||||
try {
|
||||
await POST(createMockEvent({ user: null }));
|
||||
expect.unreachable('Should have thrown redirect');
|
||||
} catch (e) {
|
||||
expect(e).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('$lib/server/prisma.js', () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/server/prisma.js';
|
||||
import { GET, PATCH } from '../+server.js';
|
||||
|
||||
const mockUser = prisma.user as unknown as {
|
||||
findUnique: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
function createMockEvent(
|
||||
overrides: {
|
||||
user?: { id: string; role: string } | null;
|
||||
body?: unknown;
|
||||
} = {}
|
||||
) {
|
||||
const { user = { id: 'u1', role: 'user' }, body = {} } = overrides;
|
||||
|
||||
return {
|
||||
locals: { user },
|
||||
request: {
|
||||
json: vi.fn().mockResolvedValue(body)
|
||||
}
|
||||
} as unknown as Parameters<typeof GET>[0];
|
||||
}
|
||||
|
||||
describe('User Preferences API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /api/users/me/preferences', () => {
|
||||
it('returns preferences for authenticated user', async () => {
|
||||
const prefs = {
|
||||
themeMode: 'dark',
|
||||
primaryHue: 240,
|
||||
primarySaturation: 80,
|
||||
backgroundType: 'none',
|
||||
locale: 'en'
|
||||
};
|
||||
mockUser.findUnique.mockResolvedValue(prefs);
|
||||
|
||||
const response = await GET(createMockEvent());
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.data).toEqual(prefs);
|
||||
});
|
||||
|
||||
it('returns 404 when user not found', async () => {
|
||||
mockUser.findUnique.mockResolvedValue(null);
|
||||
|
||||
const response = await GET(createMockEvent());
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('redirects when not authenticated', async () => {
|
||||
try {
|
||||
await GET(createMockEvent({ user: null }));
|
||||
expect.unreachable('Should have thrown redirect');
|
||||
} catch (e) {
|
||||
// SvelteKit redirect is thrown as an object with status and location
|
||||
expect(e).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/users/me/preferences', () => {
|
||||
it('updates theme preferences', async () => {
|
||||
const updatedPrefs = {
|
||||
themeMode: 'light',
|
||||
primaryHue: 120,
|
||||
primarySaturation: 60,
|
||||
backgroundType: 'mesh',
|
||||
locale: 'ru'
|
||||
};
|
||||
mockUser.update.mockResolvedValue(updatedPrefs);
|
||||
|
||||
const response = await PATCH(
|
||||
createMockEvent({
|
||||
body: {
|
||||
themeMode: 'light',
|
||||
primaryHue: 120,
|
||||
primarySaturation: 60,
|
||||
backgroundType: 'mesh',
|
||||
locale: 'ru'
|
||||
}
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.data).toEqual(updatedPrefs);
|
||||
});
|
||||
|
||||
it('rejects invalid themeMode', async () => {
|
||||
const response = await PATCH(
|
||||
createMockEvent({ body: { themeMode: 'invalid' } })
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
expect(data.error).toContain('themeMode');
|
||||
});
|
||||
|
||||
it('rejects primaryHue out of range', async () => {
|
||||
const response = await PATCH(
|
||||
createMockEvent({ body: { primaryHue: 500 } })
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects primarySaturation out of range', async () => {
|
||||
const response = await PATCH(
|
||||
createMockEvent({ body: { primarySaturation: -10 } })
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid backgroundType', async () => {
|
||||
const response = await PATCH(
|
||||
createMockEvent({ body: { backgroundType: 'invalid' } })
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid locale', async () => {
|
||||
const response = await PATCH(
|
||||
createMockEvent({ body: { locale: 'fr' } })
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects request with no valid fields', async () => {
|
||||
const response = await PATCH(
|
||||
createMockEvent({ body: { unknownField: 'value' } })
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('allows null values to reset preferences', async () => {
|
||||
mockUser.update.mockResolvedValue({
|
||||
themeMode: null,
|
||||
primaryHue: null,
|
||||
primarySaturation: null,
|
||||
backgroundType: null,
|
||||
locale: null
|
||||
});
|
||||
|
||||
const response = await PATCH(
|
||||
createMockEvent({
|
||||
body: { themeMode: null, primaryHue: null }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user