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:
parent
ee586065dd
commit
bf2785bcf9
@ -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(
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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¬eId=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);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
82
backend/src/modules/note-tasks/routes.integration.test.ts
Normal file
82
backend/src/modules/note-tasks/routes.integration.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
125
backend/src/modules/notes/routes.integration.test.ts
Normal file
125
backend/src/modules/notes/routes.integration.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
99
backend/src/modules/saved-views/routes.integration.test.ts
Normal file
99
backend/src/modules/saved-views/routes.integration.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
89
backend/src/modules/workspaces/routes.integration.test.ts
Normal file
89
backend/src/modules/workspaces/routes.integration.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
26
backend/src/test-helpers.ts
Normal file
26
backend/src/test-helpers.ts
Normal 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' };
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user