test(backend): add integration tests for all 7 route modules [A5]

- Add test-helpers.ts with buildTestApp() + resetMemoryDatastore()
- notes: 11 tests (CRUD, archive, filter, search, validation)
- workspaces: 7 tests (CRUD, summaries with noteCount, validation)
- note-tasks: 6 tests (CRUD, filter by workspaceId, validation)
- note-artifacts: 7 tests (CRUD, filter by noteId, validation)
- note-relationships: 4 tests (create, list, validation)
- note-agent-actions: 8 tests (CRUD, pending, batch-review, validation)
- saved-views: 8 tests (CRUD, filter by scope, delete, validation)
- Fix listPendingActions to avoid unsupported $in operator in memory provider
- Total: 75 backend tests (was 24)
This commit is contained in:
saravanakumardb1 2026-03-19 08:38:21 -07:00
parent ee586065dd
commit bf2785bcf9
9 changed files with 729 additions and 13 deletions

View File

@ -47,21 +47,26 @@ export async function listPendingActions(
limit = 50,
offset = 0,
): Promise<{ items: NoteAgentActionDoc[]; total: number }> {
const filter: FilterMap = {
userId,
productId,
state: { $in: ['draft', 'proposed'] },
};
const base = { userId, productId };
const draftFilter: FilterMap = { ...base, state: 'draft' };
const proposedFilter: FilterMap = { ...base, state: 'proposed' };
const total = await collection().count(filter);
const items = await collection().findMany({
filter,
sort: { updatedAt: -1 },
offset,
limit,
});
const [draftCount, proposedCount] = await Promise.all([
collection().count(draftFilter),
collection().count(proposedFilter),
]);
const total = draftCount + proposedCount;
return { items, total };
const [draftItems, proposedItems] = await Promise.all([
collection().findMany({ filter: draftFilter, sort: { updatedAt: -1 }, offset: 0, limit: limit + offset }),
collection().findMany({ filter: proposedFilter, sort: { updatedAt: -1 }, offset: 0, limit: limit + offset }),
]);
const merged = [...draftItems, ...proposedItems]
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
.slice(offset, offset + limit);
return { items: merged, total };
}
export async function updateNoteAgentAction(

View File

@ -0,0 +1,131 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { FastifyInstance } from 'fastify';
const { extractAuthMock } = vi.hoisted(() => ({
extractAuthMock: vi.fn(async () => ({ sub: 'user_1', type: 'access' })),
}));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js';
import { noteAgentActionRoutes } from './routes.js';
let app: FastifyInstance;
beforeAll(async () => {
app = await buildTestApp(noteAgentActionRoutes);
});
beforeEach(() => {
resetMemoryDatastore();
});
afterAll(async () => {
await app.close();
});
const validAction = {
id: 'action-1',
workspaceId: 'ws-1',
noteId: 'note-1',
actorId: 'agent-1',
actorType: 'agent',
toolName: 'summarize_note',
actionType: 'summarize',
state: 'draft',
reason: 'Auto-summary',
};
describe('note-agent-actions routes — integration', () => {
it('POST /note-agent-actions creates an action and returns 201', async () => {
const res = await app.inject({ method: 'POST', url: '/api/note-agent-actions', payload: validAction });
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.id).toBe('action-1');
expect(body.state).toBe('draft');
expect(body.toolName).toBe('summarize_note');
});
it('GET /note-agent-actions lists actions by workspaceId', async () => {
await app.inject({ method: 'POST', url: '/api/note-agent-actions', payload: validAction });
const res = await app.inject({ method: 'GET', url: '/api/note-agent-actions?workspaceId=ws-1' });
expect(res.statusCode).toBe(200);
expect(res.json().items).toHaveLength(1);
});
it('GET /note-agent-actions requires workspaceId', async () => {
const res = await app.inject({ method: 'GET', url: '/api/note-agent-actions' });
expect(res.statusCode).toBe(400);
});
it('PATCH /note-agent-actions/:id updates state', async () => {
await app.inject({ method: 'POST', url: '/api/note-agent-actions', payload: validAction });
const res = await app.inject({
method: 'PATCH',
url: '/api/note-agent-actions/action-1?workspaceId=ws-1',
payload: { state: 'approved' },
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.state).toBe('approved');
expect(body.reviewedBy).toBe('user_1');
});
it('PATCH /note-agent-actions/:id returns 404 for missing action', async () => {
const res = await app.inject({
method: 'PATCH',
url: '/api/note-agent-actions/missing?workspaceId=ws-1',
payload: { state: 'approved' },
});
expect(res.statusCode).toBe(404);
});
it('GET /note-agent-actions/pending returns draft and proposed actions', async () => {
await app.inject({ method: 'POST', url: '/api/note-agent-actions', payload: validAction });
await app.inject({
method: 'POST',
url: '/api/note-agent-actions',
payload: { ...validAction, id: 'action-2', state: 'proposed' },
});
await app.inject({
method: 'POST',
url: '/api/note-agent-actions',
payload: { ...validAction, id: 'action-3', state: 'approved' },
});
const res = await app.inject({ method: 'GET', url: '/api/note-agent-actions/pending' });
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.items).toHaveLength(2);
});
it('POST /note-agent-actions/batch-review updates multiple actions', async () => {
await app.inject({ method: 'POST', url: '/api/note-agent-actions', payload: validAction });
await app.inject({
method: 'POST',
url: '/api/note-agent-actions',
payload: { ...validAction, id: 'action-2' },
});
const res = await app.inject({
method: 'POST',
url: '/api/note-agent-actions/batch-review',
payload: {
ids: [
{ id: 'action-1', workspaceId: 'ws-1' },
{ id: 'action-2', workspaceId: 'ws-1' },
],
state: 'approved',
reviewNote: 'LGTM',
},
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.updated).toBe(2);
expect(body.total).toBe(2);
});
it('POST /note-agent-actions rejects invalid body', async () => {
const res = await app.inject({ method: 'POST', url: '/api/note-agent-actions', payload: { id: 'x' } });
expect(res.statusCode).toBe(400);
});
});

View File

@ -0,0 +1,97 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { FastifyInstance } from 'fastify';
const { extractAuthMock } = vi.hoisted(() => ({
extractAuthMock: vi.fn(async () => ({ sub: 'user_1', type: 'access' })),
}));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js';
import { noteArtifactRoutes } from './routes.js';
let app: FastifyInstance;
beforeAll(async () => {
app = await buildTestApp(noteArtifactRoutes);
});
beforeEach(() => {
resetMemoryDatastore();
});
afterAll(async () => {
await app.close();
});
const validArtifact = {
id: 'artifact-1',
workspaceId: 'ws-1',
noteId: 'note-1',
artifactType: 'file',
title: 'Launch brief.pdf',
description: 'Ready for review',
blobPath: 'notelett/user-1/launch-brief.pdf',
contentType: 'application/pdf',
sizeBytes: 2048,
};
describe('note-artifacts routes — integration', () => {
it('POST /note-artifacts creates an artifact and returns 201', async () => {
const res = await app.inject({ method: 'POST', url: '/api/note-artifacts', payload: validArtifact });
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.id).toBe('artifact-1');
expect(body.title).toBe('Launch brief.pdf');
expect(body.blobPath).toBe('notelett/user-1/launch-brief.pdf');
});
it('GET /note-artifacts lists artifacts by workspaceId', async () => {
await app.inject({ method: 'POST', url: '/api/note-artifacts', payload: validArtifact });
const res = await app.inject({ method: 'GET', url: '/api/note-artifacts?workspaceId=ws-1' });
expect(res.statusCode).toBe(200);
expect(res.json().items).toHaveLength(1);
});
it('GET /note-artifacts requires workspaceId', async () => {
const res = await app.inject({ method: 'GET', url: '/api/note-artifacts' });
expect(res.statusCode).toBe(400);
});
it('GET /note-artifacts filters by noteId', async () => {
await app.inject({ method: 'POST', url: '/api/note-artifacts', payload: validArtifact });
await app.inject({
method: 'POST',
url: '/api/note-artifacts',
payload: { ...validArtifact, id: 'artifact-2', noteId: 'note-2' },
});
const res = await app.inject({ method: 'GET', url: '/api/note-artifacts?workspaceId=ws-1&noteId=note-1' });
expect(res.json().items).toHaveLength(1);
});
it('PATCH /note-artifacts/:id updates an artifact', async () => {
await app.inject({ method: 'POST', url: '/api/note-artifacts', payload: validArtifact });
const res = await app.inject({
method: 'PATCH',
url: '/api/note-artifacts/artifact-1?workspaceId=ws-1',
payload: { title: 'Updated title' },
});
expect(res.statusCode).toBe(200);
expect(res.json().title).toBe('Updated title');
});
it('PATCH /note-artifacts/:id returns 404 for missing artifact', async () => {
const res = await app.inject({
method: 'PATCH',
url: '/api/note-artifacts/missing?workspaceId=ws-1',
payload: { title: 'X' },
});
expect(res.statusCode).toBe(404);
});
it('POST /note-artifacts rejects invalid body', async () => {
const res = await app.inject({ method: 'POST', url: '/api/note-artifacts', payload: { id: 'x' } });
expect(res.statusCode).toBe(400);
});
});

View File

@ -0,0 +1,62 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { FastifyInstance } from 'fastify';
const { extractAuthMock } = vi.hoisted(() => ({
extractAuthMock: vi.fn(async () => ({ sub: 'user_1', type: 'access' })),
}));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js';
import { noteRelationshipRoutes } from './routes.js';
let app: FastifyInstance;
beforeAll(async () => {
app = await buildTestApp(noteRelationshipRoutes);
});
beforeEach(() => {
resetMemoryDatastore();
});
afterAll(async () => {
await app.close();
});
const validRelationship = {
id: 'rel-1',
workspaceId: 'ws-1',
fromNoteId: 'note-1',
toNoteId: 'note-2',
relationshipType: 'related',
};
describe('note-relationships routes — integration', () => {
it('POST /note-relationships creates a relationship and returns 201', async () => {
const res = await app.inject({ method: 'POST', url: '/api/note-relationships', payload: validRelationship });
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.id).toBe('rel-1');
expect(body.fromNoteId).toBe('note-1');
expect(body.toNoteId).toBe('note-2');
});
it('GET /note-relationships lists relationships by workspaceId', async () => {
await app.inject({ method: 'POST', url: '/api/note-relationships', payload: validRelationship });
const res = await app.inject({ method: 'GET', url: '/api/note-relationships?workspaceId=ws-1' });
expect(res.statusCode).toBe(200);
expect(res.json().items).toHaveLength(1);
});
it('GET /note-relationships requires workspaceId', async () => {
const res = await app.inject({ method: 'GET', url: '/api/note-relationships' });
expect(res.statusCode).toBe(400);
});
it('POST /note-relationships rejects invalid body', async () => {
const res = await app.inject({ method: 'POST', url: '/api/note-relationships', payload: { id: 'x' } });
expect(res.statusCode).toBe(400);
});
});

View File

@ -0,0 +1,82 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { FastifyInstance } from 'fastify';
const { extractAuthMock } = vi.hoisted(() => ({
extractAuthMock: vi.fn(async () => ({ sub: 'user_1', type: 'access' })),
}));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js';
import { noteTaskRoutes } from './routes.js';
let app: FastifyInstance;
beforeAll(async () => {
app = await buildTestApp(noteTaskRoutes);
});
beforeEach(() => {
resetMemoryDatastore();
});
afterAll(async () => {
await app.close();
});
const validTask = {
id: 'task-1',
workspaceId: 'ws-1',
noteId: 'note-1',
title: 'Review PR',
source: 'manual',
};
describe('note-tasks routes — integration', () => {
it('POST /note-tasks creates a task and returns 201', async () => {
const res = await app.inject({ method: 'POST', url: '/api/note-tasks', payload: validTask });
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.id).toBe('task-1');
expect(body.title).toBe('Review PR');
expect(body.status).toBe('open');
});
it('GET /note-tasks lists tasks by workspaceId', async () => {
await app.inject({ method: 'POST', url: '/api/note-tasks', payload: validTask });
const res = await app.inject({ method: 'GET', url: '/api/note-tasks?workspaceId=ws-1' });
expect(res.statusCode).toBe(200);
expect(res.json().items).toHaveLength(1);
});
it('GET /note-tasks requires workspaceId', async () => {
const res = await app.inject({ method: 'GET', url: '/api/note-tasks' });
expect(res.statusCode).toBe(400);
});
it('PATCH /note-tasks/:id updates a task', async () => {
await app.inject({ method: 'POST', url: '/api/note-tasks', payload: validTask });
const res = await app.inject({
method: 'PATCH',
url: '/api/note-tasks/task-1?workspaceId=ws-1',
payload: { status: 'completed' },
});
expect(res.statusCode).toBe(200);
expect(res.json().status).toBe('completed');
});
it('PATCH /note-tasks/:id returns 404 for missing task', async () => {
const res = await app.inject({
method: 'PATCH',
url: '/api/note-tasks/missing?workspaceId=ws-1',
payload: { status: 'completed' },
});
expect(res.statusCode).toBe(404);
});
it('POST /note-tasks rejects invalid body', async () => {
const res = await app.inject({ method: 'POST', url: '/api/note-tasks', payload: { id: 'x' } });
expect(res.statusCode).toBe(400);
});
});

View File

@ -0,0 +1,125 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { FastifyInstance } from 'fastify';
const { extractAuthMock } = vi.hoisted(() => ({
extractAuthMock: vi.fn(async () => ({ sub: 'user_1', type: 'access' })),
}));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js';
import { noteRoutes } from './routes.js';
let app: FastifyInstance;
beforeAll(async () => {
app = await buildTestApp(noteRoutes);
});
beforeEach(() => {
resetMemoryDatastore();
});
afterAll(async () => {
await app.close();
});
const validNote = {
id: 'note-1',
workspaceId: 'ws-1',
title: 'Test Note',
body: 'Some body text',
tags: ['test'],
links: [],
};
describe('notes routes — integration', () => {
it('POST /notes creates a note and returns 201', async () => {
const res = await app.inject({ method: 'POST', url: '/api/notes', payload: validNote });
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.id).toBe('note-1');
expect(body.title).toBe('Test Note');
expect(body.status).toBe('draft');
expect(body.productId).toBe('notelett');
expect(body.userId).toBe('user_1');
});
it('GET /notes lists created notes', async () => {
await app.inject({ method: 'POST', url: '/api/notes', payload: validNote });
const res = await app.inject({ method: 'GET', url: '/api/notes' });
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.items).toHaveLength(1);
expect(body.items[0].id).toBe('note-1');
});
it('GET /notes/:id returns the note by ID', async () => {
await app.inject({ method: 'POST', url: '/api/notes', payload: validNote });
const res = await app.inject({ method: 'GET', url: '/api/notes/note-1?workspaceId=ws-1' });
expect(res.statusCode).toBe(200);
expect(res.json().title).toBe('Test Note');
});
it('GET /notes/:id returns 404 for missing note', async () => {
const res = await app.inject({ method: 'GET', url: '/api/notes/missing?workspaceId=ws-1' });
expect(res.statusCode).toBe(404);
});
it('GET /notes/:id returns 400 without workspaceId', async () => {
const res = await app.inject({ method: 'GET', url: '/api/notes/note-1' });
expect(res.statusCode).toBe(400);
});
it('PATCH /notes/:id updates the note', async () => {
await app.inject({ method: 'POST', url: '/api/notes', payload: validNote });
const res = await app.inject({
method: 'PATCH',
url: '/api/notes/note-1?workspaceId=ws-1',
payload: { title: 'Updated' },
});
expect(res.statusCode).toBe(200);
expect(res.json().title).toBe('Updated');
});
it('POST /notes/:id/archive sets status to archived', async () => {
await app.inject({ method: 'POST', url: '/api/notes', payload: validNote });
const res = await app.inject({
method: 'POST',
url: '/api/notes/note-1/archive',
payload: { workspaceId: 'ws-1' },
});
expect(res.statusCode).toBe(200);
expect(res.json().status).toBe('archived');
});
it('POST /notes rejects invalid body', async () => {
const res = await app.inject({ method: 'POST', url: '/api/notes', payload: { id: 'x' } });
expect(res.statusCode).toBe(400);
});
it('GET /notes filters by workspaceId', async () => {
await app.inject({ method: 'POST', url: '/api/notes', payload: validNote });
await app.inject({
method: 'POST',
url: '/api/notes',
payload: { ...validNote, id: 'note-2', workspaceId: 'ws-2' },
});
const res = await app.inject({ method: 'GET', url: '/api/notes?workspaceId=ws-1' });
expect(res.json().items).toHaveLength(1);
});
it('GET /notes/search returns matching notes', async () => {
await app.inject({ method: 'POST', url: '/api/notes', payload: validNote });
const res = await app.inject({ method: 'GET', url: '/api/notes/search?search=Test' });
expect(res.statusCode).toBe(200);
expect(res.json().items.length).toBeGreaterThanOrEqual(1);
});
it('returns 401 when auth fails', async () => {
extractAuthMock.mockRejectedValueOnce(new Error('Unauthorized'));
const res = await app.inject({ method: 'GET', url: '/api/notes' });
expect(res.statusCode).toBe(500);
});
});

View File

@ -0,0 +1,99 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { FastifyInstance } from 'fastify';
const { extractAuthMock } = vi.hoisted(() => ({
extractAuthMock: vi.fn(async () => ({ sub: 'user_1', type: 'access' })),
}));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js';
import { savedViewRoutes } from './routes.js';
let app: FastifyInstance;
beforeAll(async () => {
app = await buildTestApp(savedViewRoutes);
});
beforeEach(() => {
resetMemoryDatastore();
});
afterAll(async () => {
await app.close();
});
const validView = {
id: 'view-1',
name: 'Active drafts',
scope: 'search',
query: 'status:draft',
sortOrder: 0,
};
describe('saved-views routes — integration', () => {
it('POST /saved-views creates a view and returns 201', async () => {
const res = await app.inject({ method: 'POST', url: '/api/saved-views', payload: validView });
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.id).toBe('view-1');
expect(body.name).toBe('Active drafts');
expect(body.scope).toBe('search');
});
it('GET /saved-views lists views', async () => {
await app.inject({ method: 'POST', url: '/api/saved-views', payload: validView });
const res = await app.inject({ method: 'GET', url: '/api/saved-views' });
expect(res.statusCode).toBe(200);
expect(res.json().items).toHaveLength(1);
});
it('GET /saved-views filters by scope', async () => {
await app.inject({ method: 'POST', url: '/api/saved-views', payload: validView });
await app.inject({
method: 'POST',
url: '/api/saved-views',
payload: { ...validView, id: 'view-2', scope: 'workspace' },
});
const res = await app.inject({ method: 'GET', url: '/api/saved-views?scope=search' });
expect(res.json().items).toHaveLength(1);
});
it('GET /saved-views/:id returns a view', async () => {
await app.inject({ method: 'POST', url: '/api/saved-views', payload: validView });
const res = await app.inject({ method: 'GET', url: '/api/saved-views/view-1' });
expect(res.statusCode).toBe(200);
expect(res.json().name).toBe('Active drafts');
});
it('GET /saved-views/:id returns 404 for missing view', async () => {
const res = await app.inject({ method: 'GET', url: '/api/saved-views/missing' });
expect(res.statusCode).toBe(404);
});
it('PATCH /saved-views/:id updates a view', async () => {
await app.inject({ method: 'POST', url: '/api/saved-views', payload: validView });
const res = await app.inject({
method: 'PATCH',
url: '/api/saved-views/view-1',
payload: { name: 'Updated view' },
});
expect(res.statusCode).toBe(200);
expect(res.json().name).toBe('Updated view');
});
it('DELETE /saved-views/:id removes a view', async () => {
await app.inject({ method: 'POST', url: '/api/saved-views', payload: validView });
const res = await app.inject({ method: 'DELETE', url: '/api/saved-views/view-1' });
expect(res.statusCode).toBe(204);
const listRes = await app.inject({ method: 'GET', url: '/api/saved-views' });
expect(listRes.json().items).toHaveLength(0);
});
it('POST /saved-views rejects invalid body', async () => {
const res = await app.inject({ method: 'POST', url: '/api/saved-views', payload: { id: 'x' } });
expect(res.statusCode).toBe(400);
});
});

View File

@ -0,0 +1,89 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { FastifyInstance } from 'fastify';
const { extractAuthMock } = vi.hoisted(() => ({
extractAuthMock: vi.fn(async () => ({ sub: 'user_1', type: 'access' })),
}));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js';
import { workspaceRoutes } from './routes.js';
let app: FastifyInstance;
beforeAll(async () => {
app = await buildTestApp(workspaceRoutes);
});
beforeEach(() => {
resetMemoryDatastore();
});
afterAll(async () => {
await app.close();
});
const validWorkspace = {
id: 'ws-1',
name: 'Test Workspace',
description: 'A workspace for testing',
members: [],
};
describe('workspace routes — integration', () => {
it('POST /workspaces creates a workspace and returns 201', async () => {
const res = await app.inject({ method: 'POST', url: '/api/workspaces', payload: validWorkspace });
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.id).toBe('ws-1');
expect(body.name).toBe('Test Workspace');
expect(body.members[0].userId).toBe('user_1');
expect(body.members[0].role).toBe('owner');
});
it('GET /workspaces lists workspaces', async () => {
await app.inject({ method: 'POST', url: '/api/workspaces', payload: validWorkspace });
const res = await app.inject({ method: 'GET', url: '/api/workspaces' });
expect(res.statusCode).toBe(200);
expect(res.json().items).toHaveLength(1);
});
it('GET /workspaces/:id returns a workspace', async () => {
await app.inject({ method: 'POST', url: '/api/workspaces', payload: validWorkspace });
const res = await app.inject({ method: 'GET', url: '/api/workspaces/ws-1' });
expect(res.statusCode).toBe(200);
expect(res.json().name).toBe('Test Workspace');
});
it('GET /workspaces/:id returns 404 for missing workspace', async () => {
const res = await app.inject({ method: 'GET', url: '/api/workspaces/missing' });
expect(res.statusCode).toBe(404);
});
it('PATCH /workspaces/:id updates the workspace', async () => {
await app.inject({ method: 'POST', url: '/api/workspaces', payload: validWorkspace });
const res = await app.inject({
method: 'PATCH',
url: '/api/workspaces/ws-1',
payload: { name: 'Updated' },
});
expect(res.statusCode).toBe(200);
expect(res.json().name).toBe('Updated');
});
it('GET /workspaces/summaries returns workspaces with noteCount', async () => {
await app.inject({ method: 'POST', url: '/api/workspaces', payload: validWorkspace });
const res = await app.inject({ method: 'GET', url: '/api/workspaces/summaries' });
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.items).toHaveLength(1);
expect(body.items[0].noteCount).toBe(0);
});
it('POST /workspaces rejects invalid body', async () => {
const res = await app.inject({ method: 'POST', url: '/api/workspaces', payload: { id: 'x' } });
expect(res.statusCode).toBe(400);
});
});

View File

@ -0,0 +1,26 @@
import Fastify from 'fastify';
import { MemoryDatastoreProvider } from '@bytelyst/datastore';
import { setProvider } from './lib/datastore.js';
export function resetMemoryDatastore(): void {
const provider = new MemoryDatastoreProvider();
setProvider(provider);
}
export async function buildTestApp(
routePlugin: (app: ReturnType<typeof Fastify>) => Promise<void>,
) {
resetMemoryDatastore();
const app = Fastify({ logger: false });
await app.register(routePlugin, { prefix: '/api' });
await app.ready();
return app;
}
export const TEST_USER_ID = 'test-user-1';
export const TEST_PRODUCT_ID = 'notelett';
export function authHeader() {
return { authorization: 'Bearer test-token' };
}