test: add Vitest setup with 115 tests (backend + frontend)

This commit is contained in:
2026-06-26 14:19:50 +02:00
parent 19884b7b5d
commit f853372c90
11 changed files with 2062 additions and 42 deletions
+269
View File
@@ -0,0 +1,269 @@
/**
* Auth API Tests Register / Login / Logout / Me / Password Change
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import type { FastifyInstance } from 'fastify';
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
import { createServer } from '../src/server.js';
describe('Auth API', () => {
let app: FastifyInstance;
let db: SqliteAdapter;
let authToken: string | null = null;
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();
});
// ─── Register ────────────────────────────────────────
describe('POST /api/auth/register', () => {
it('should register a new user with 201', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/register',
payload: {
email: 'testuser@example.com',
password: 'TestPass123!',
name: 'Test User',
role: 'planer',
},
});
expect(response.statusCode).toBe(201);
const body = JSON.parse(response.body);
expect(body.user).toBeDefined();
expect(body.user.email).toBe('testuser@example.com');
expect(body.user.name).toBe('Test User');
expect(body.user.password_hash).toBeUndefined();
expect(body.session).toBeDefined();
expect(body.session.token).toBeDefined();
authToken = body.session.token;
});
it('should reject duplicate registration with 409', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/register',
payload: {
email: 'testuser@example.com',
password: 'TestPass123!',
name: 'Test User 2',
},
});
expect(response.statusCode).toBe(409);
const body = JSON.parse(response.body);
expect(body.error).toContain('already registered');
});
it('should reject missing fields with 400', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/register',
payload: { email: 'incomplete@example.com' },
});
expect(response.statusCode).toBe(400);
});
});
// ─── Login ──────────────────────────────────────────
describe('POST /api/auth/login', () => {
it('should login with correct credentials', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'testuser@example.com',
password: 'TestPass123!',
},
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.user).toBeDefined();
expect(body.session.token).toBeDefined();
authToken = body.session.token;
});
it('should reject wrong password with 401', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'testuser@example.com',
password: 'WrongPassword!',
},
});
expect(response.statusCode).toBe(401);
});
it('should reject non-existent email with 401', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'nobody@example.com',
password: 'SomePassword!',
},
});
expect(response.statusCode).toBe(401);
});
it('should reject missing fields with 400', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: { email: 'testuser@example.com' },
});
expect(response.statusCode).toBe(400);
});
});
// ─── Me ─────────────────────────────────────────────
describe('GET /api/auth/me', () => {
it('should return current user profile with valid token', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/auth/me',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.email).toBe('testuser@example.com');
expect(body.password_hash).toBeUndefined();
});
it('should reject without token with 401', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/auth/me',
});
expect(response.statusCode).toBe(401);
});
it('should reject invalid token with 401', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/auth/me',
headers: { authorization: 'Bearer invalid-token-xxx' },
});
expect(response.statusCode).toBe(401);
});
});
// ─── Password Change ───────────────────────────────
describe('PATCH /api/auth/password', () => {
it('should change password with correct old password', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/auth/password',
headers: { authorization: `Bearer ${authToken}` },
payload: {
oldPassword: 'TestPass123!',
newPassword: 'NewPassword456!',
},
});
expect(response.statusCode).toBe(204);
});
it('should allow login with new password', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'testuser@example.com',
password: 'NewPassword456!',
},
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.session.token).toBeDefined();
authToken = body.session.token;
});
it('should reject old password after change', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'testuser@example.com',
password: 'TestPass123!',
},
});
expect(response.statusCode).toBe(401);
});
it('should reject password change with wrong old password', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/auth/password',
headers: { authorization: `Bearer ${authToken}` },
payload: {
oldPassword: 'WrongOldPassword!',
newPassword: 'AnotherNew789!',
},
});
expect(response.statusCode).toBe(400);
});
it('should reject password change without auth', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/auth/password',
payload: {
oldPassword: 'NewPassword456!',
newPassword: 'AnotherNew789!',
},
});
expect(response.statusCode).toBe(401);
});
it('should reject missing password fields', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/auth/password',
headers: { authorization: `Bearer ${authToken}` },
payload: { oldPassword: 'NewPassword456!' },
});
expect(response.statusCode).toBe(400);
});
});
// ─── Logout ─────────────────────────────────────────
describe('POST /api/auth/logout', () => {
it('should logout and invalidate token', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/logout',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(204);
// Token should now be invalid
const meResponse = await app.inject({
method: 'GET',
url: '/api/auth/me',
headers: { authorization: `Bearer ${authToken}` },
});
expect(meResponse.statusCode).toBe(401);
});
it('should return 204 even without token', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/logout',
});
expect(response.statusCode).toBe(204);
});
});
});