02308dc54a
- Fix 2 backend test failures (projectFolders validation, StressTest timeout) - Fix frontend test dependency (@testing-library/dom) - Delete App.tsx.bak from repo - Add rate limiting on auth endpoints (login: 10/min, register: 3/min) - Add .env.example with all environment variables - Update BAUPLAN.md with accurate phase status (16/21 phases implemented) - Add OpenAPI/Swagger documentation (docs/openapi.yaml) - Add E2E workflow test (15 steps: register→project→drawing→layer→element→CRUD→delete) - Add Forgejo CI/CD pipeline (.forgejo/workflows/ci.yml) - Improve .gitignore (db-wal, .env.*, *.bak, coverage) - Add security comment for default user in schema.sql All 628 tests passing (254 backend + 374 frontend)
203 lines
6.1 KiB
TypeScript
203 lines
6.1 KiB
TypeScript
/**
|
|
* E2E Workflow Test: Login → Create Project → Create Drawing → Create Layer → Create Element → Update → Delete
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
|
import { createServer } from '../src/server.js';
|
|
|
|
describe('E2E: Full CAD Workflow', () => {
|
|
let app: FastifyInstance;
|
|
let db: SqliteAdapter;
|
|
let authToken: string;
|
|
let projectId: string;
|
|
let drawingId: string;
|
|
let layerId: string;
|
|
let elementId: string;
|
|
|
|
beforeAll(async () => {
|
|
db = new SqliteAdapter(':memory:');
|
|
await db.init();
|
|
app = await createServer({ db, port: 0 });
|
|
await app.ready();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
db.close();
|
|
});
|
|
|
|
it('Step 1: Register a new user', async () => {
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/auth/register',
|
|
payload: { email: 'e2e@example.com', password: 'Password123!', name: 'E2E Tester' },
|
|
});
|
|
expect(res.statusCode).toBe(201);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.session.token).toBeDefined();
|
|
authToken = body.session.token;
|
|
});
|
|
|
|
it('Step 2: Get user profile', async () => {
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: '/api/auth/me',
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.email).toBe('e2e@example.com');
|
|
});
|
|
|
|
it('Step 3: Create a project', async () => {
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/projects',
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
payload: { name: 'E2E Test Project', description: 'Full workflow test' },
|
|
});
|
|
expect(res.statusCode).toBe(201);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.name).toBe('E2E Test Project');
|
|
projectId = body.id;
|
|
});
|
|
|
|
it('Step 4: Create a drawing in the project', async () => {
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: `/api/projects/${projectId}/drawings`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
payload: { name: 'Test Drawing' },
|
|
});
|
|
expect(res.statusCode).toBe(201);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.name).toBe('Test Drawing');
|
|
drawingId = body.id;
|
|
});
|
|
|
|
it('Step 5: Create a layer in the drawing', async () => {
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: `/api/drawings/${drawingId}/layers`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
payload: { name: 'Test Layer', color: '#ff0000' },
|
|
});
|
|
expect(res.statusCode).toBe(201);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.name).toBe('Test Layer');
|
|
layerId = body.id;
|
|
});
|
|
|
|
it('Step 6: Create an element on the layer', async () => {
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: `/api/drawings/${drawingId}/elements`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
payload: {
|
|
layer_id: layerId,
|
|
type: 'rect',
|
|
x: 10, y: 20, width: 100, height: 50,
|
|
properties_json: JSON.stringify({ stroke: '#0000ff', fill: '#cccccc' }),
|
|
},
|
|
});
|
|
expect(res.statusCode).toBe(201);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.type).toBe('rect');
|
|
elementId = body.id;
|
|
});
|
|
|
|
it('Step 7: List elements in the drawing', async () => {
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: `/api/drawings/${drawingId}/elements`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.length).toBe(1);
|
|
expect(body[0].id).toBe(elementId);
|
|
});
|
|
|
|
it('Step 8: Update the element', async () => {
|
|
const res = await app.inject({
|
|
method: 'PATCH',
|
|
url: `/api/elements/${elementId}`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
payload: { x: 50, y: 60, width: 200, height: 100 },
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.x).toBe(50);
|
|
expect(body.width).toBe(200);
|
|
});
|
|
|
|
it('Step 9: Update the layer visibility', async () => {
|
|
const res = await app.inject({
|
|
method: 'PATCH',
|
|
url: `/api/layers/${layerId}`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
payload: { visible: 0 },
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.visible).toBe(0);
|
|
});
|
|
|
|
it('Step 10: Delete the element', async () => {
|
|
const res = await app.inject({
|
|
method: 'DELETE',
|
|
url: `/api/elements/${elementId}`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
});
|
|
expect(res.statusCode).toBe(204);
|
|
});
|
|
|
|
it('Step 11: Verify element is deleted', async () => {
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: `/api/drawings/${drawingId}/elements`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.length).toBe(0);
|
|
});
|
|
|
|
it('Step 12: Delete the drawing', async () => {
|
|
const res = await app.inject({
|
|
method: 'DELETE',
|
|
url: `/api/drawings/${drawingId}`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
});
|
|
expect(res.statusCode).toBe(204);
|
|
});
|
|
|
|
it('Step 13: Delete the project', async () => {
|
|
const res = await app.inject({
|
|
method: 'DELETE',
|
|
url: `/api/projects/${projectId}`,
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
});
|
|
expect(res.statusCode).toBe(204);
|
|
});
|
|
|
|
it('Step 14: Logout', async () => {
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/auth/logout',
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
});
|
|
expect(res.statusCode).toBe(204);
|
|
});
|
|
|
|
it('Step 15: Verify token is invalid after logout', async () => {
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: '/api/auth/me',
|
|
headers: { authorization: `Bearer ${authToken}` },
|
|
});
|
|
expect(res.statusCode).toBe(401);
|
|
});
|
|
});
|