- Custom error classes (lib/errors.ts) and signed-cookie session (lib/auth/session.ts, HMAC-SHA256, httpOnly+sameSite=lax) - UserRepository + AuthService (bcrypt hashing, role/status checks, register/login/logout/getCurrentUser/updateProfile) - Auth helpers (getCurrentUser), user validator, API error handler - API routes: register (auto-login), login, logout, me, PATCH users/me, GET admin/migrations (admin-only guard) - middleware.ts redirects unauthenticated page access to /login - Screens: login (login/register toggle), profile, dashboard skeleton, home redirect - vitest: exclude e2e specs, setupFiles for SESSION_SECRET - playwright: run migrate before dev server, set SESSION_SECRET - Unit tests (UserRepository, AuthService, session), integration auth-flow, e2e auth.spec (register/login/logout/protected/401)
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { createMigratedTestDb } from '@/tests/helpers/db';
|
|
import type { SqliteDatabase } from '@/lib/db/sqlite';
|
|
import { UserRepository } from '@/repositories/UserRepository';
|
|
import { AuthService } from '@/services/AuthService';
|
|
import { verifySessionToken } from '@/lib/auth/session';
|
|
import { UnauthorizedError } from '@/lib/errors';
|
|
|
|
describe('authentication flow (integration)', () => {
|
|
let db: SqliteDatabase;
|
|
let repo: UserRepository;
|
|
let authService: AuthService;
|
|
|
|
beforeEach(() => {
|
|
db = createMigratedTestDb();
|
|
repo = new UserRepository(db);
|
|
authService = new AuthService(repo);
|
|
});
|
|
|
|
afterEach(() => {
|
|
db.close();
|
|
});
|
|
|
|
it('registers, logs in, resolves the session, and reads the user', () => {
|
|
// 1. 登録
|
|
const registered = authService.register({
|
|
name: 'Flow User',
|
|
email: 'flow@example.com',
|
|
password: 'password123',
|
|
});
|
|
expect(registered.id).toBeGreaterThan(0);
|
|
|
|
// 2. ログイン → トークン発行
|
|
const { user, token } = authService.login(
|
|
'flow@example.com',
|
|
'password123'
|
|
);
|
|
expect(user.id).toBe(registered.id);
|
|
|
|
// 3. トークンからユーザーIDを解決(セッション復元)
|
|
const resolvedUserId = verifySessionToken(token);
|
|
expect(resolvedUserId).toBe(user.id);
|
|
|
|
// 4. ユーザーIDから現在ユーザーを取得(getCurrentUser相当)
|
|
const current = repo.findById(resolvedUserId!);
|
|
expect(current?.email).toBe('flow@example.com');
|
|
});
|
|
|
|
it('rejects login after the account is deactivated', () => {
|
|
authService.register({
|
|
name: 'Flow User',
|
|
email: 'flow@example.com',
|
|
password: 'password123',
|
|
});
|
|
|
|
// 一度ログイン成功を確認
|
|
expect(() =>
|
|
authService.login('flow@example.com', 'password123')
|
|
).not.toThrow();
|
|
|
|
// 無効化
|
|
const user = repo.findByEmail('flow@example.com')!;
|
|
repo.update(user.id, { status: 'inactive' });
|
|
|
|
// 無効アカウントではログイン不可
|
|
expect(() => authService.login('flow@example.com', 'password123')).toThrow(
|
|
UnauthorizedError
|
|
);
|
|
});
|
|
|
|
it('prevents a session token from resolving after profile email changes', () => {
|
|
const created = authService.register({
|
|
name: 'Flow User',
|
|
email: 'flow@example.com',
|
|
password: 'password123',
|
|
});
|
|
const { token } = authService.login('flow@example.com', 'password123');
|
|
expect(verifySessionToken(token)).toBe(created.id);
|
|
|
|
// プロフィール更新(メール変更)後もトークンはuidベースで有効
|
|
authService.updateProfile(created.id, {
|
|
email: 'changed@example.com',
|
|
name: 'New Name',
|
|
});
|
|
|
|
const resolved = repo.findById(verifySessionToken(token)!);
|
|
expect(resolved?.email).toBe('changed@example.com');
|
|
});
|
|
});
|