eec1653ff4
* fix(security): isolate dependency hotfix from github main * fix(security): expose hotfix jwt decorators across routes * test(e2e): restore stable app header selectors * test(e2e): align planner and app shell checks * test(e2e): add legacy settings page selectors * test(e2e): align settings page contracts
863 lines
22 KiB
TypeScript
863 lines
22 KiB
TypeScript
/**
|
|
* E2E Tests for auth routes with AUTH_ENABLED=true
|
|
*/
|
|
|
|
import cookie from "@fastify/cookie";
|
|
import sensible from "@fastify/sensible";
|
|
import type { Client } from "@libsql/client";
|
|
import Fastify, { type FastifyInstance } from "fastify";
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { jwtPlugin } from "../plugins/jwt.js";
|
|
import { documentationSchemaAjv } from "../utils/documentation-schema-keywords.js";
|
|
|
|
// Use vi.hoisted to create the db BEFORE mocks are set up
|
|
const { testClient, testDb } = vi.hoisted(() => {
|
|
const { createClient } = require("@libsql/client");
|
|
const { drizzle } = require("drizzle-orm/libsql");
|
|
const client = createClient({ url: ":memory:" });
|
|
const db = drizzle(client);
|
|
return { testClient: client, testDb: db };
|
|
});
|
|
|
|
// Mock modules using the hoisted db
|
|
vi.mock("../db/client.js", () => ({
|
|
db: testDb,
|
|
migrationsReady: Promise.resolve(),
|
|
}));
|
|
|
|
// Enable auth for these tests
|
|
vi.mock("../plugins/env.js", () => ({
|
|
env: {
|
|
AUTH_ENABLED: true,
|
|
FORM_LOGIN_ENABLED: true,
|
|
REGISTRATION_ENABLED: true,
|
|
OIDC_ENABLED: false,
|
|
NODE_ENV: "test",
|
|
LOG_LEVEL: "silent",
|
|
PORT: 3000,
|
|
CORS_ORIGINS: "*",
|
|
JWT_SECRET: "test-jwt-secret-12345",
|
|
REFRESH_SECRET: "test-refresh-secret-12345",
|
|
COOKIE_SECRET: "test-cookie-secret-12345",
|
|
ACCESS_TOKEN_TTL_MINUTES: 15,
|
|
REFRESH_TOKEN_TTL_DAYS: 7,
|
|
},
|
|
}));
|
|
|
|
// Import real auth plugin and routes
|
|
const { authRoutes } = await import("../routes/auth.js");
|
|
|
|
// =============================================================================
|
|
// Test Setup
|
|
// =============================================================================
|
|
|
|
async function createSchema(client: Client) {
|
|
const tableCreations = [
|
|
`CREATE TABLE IF NOT EXISTS users (
|
|
id integer PRIMARY KEY AUTOINCREMENT,
|
|
username text NOT NULL UNIQUE,
|
|
password_hash text,
|
|
avatar_url text,
|
|
auth_provider text NOT NULL DEFAULT 'local',
|
|
oidc_subject text,
|
|
is_active integer NOT NULL DEFAULT 1,
|
|
last_login_at integer,
|
|
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
|
|
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS refresh_tokens (
|
|
id integer PRIMARY KEY AUTOINCREMENT,
|
|
user_id integer NOT NULL,
|
|
token_id text NOT NULL UNIQUE,
|
|
expires_at integer NOT NULL,
|
|
revoked integer NOT NULL DEFAULT 0,
|
|
rotated_at integer,
|
|
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)`,
|
|
];
|
|
|
|
for (const sql of tableCreations) {
|
|
await client.execute(sql);
|
|
}
|
|
}
|
|
|
|
async function clearData(client: Client) {
|
|
await client.execute("DELETE FROM refresh_tokens");
|
|
await client.execute("DELETE FROM users");
|
|
await client.execute("DELETE FROM sqlite_sequence");
|
|
}
|
|
|
|
// =============================================================================
|
|
// Tests
|
|
// =============================================================================
|
|
|
|
describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
|
let app: FastifyInstance;
|
|
|
|
beforeAll(async () => {
|
|
await createSchema(testClient);
|
|
|
|
app = Fastify({ logger: false, ajv: documentationSchemaAjv });
|
|
|
|
await app.register(sensible);
|
|
await app.register(cookie, { secret: "test-cookie-secret-12345" });
|
|
await app.register(jwtPlugin, {
|
|
secret: "test-jwt-secret-12345",
|
|
cookie: { cookieName: "access_token", signed: false },
|
|
});
|
|
|
|
// Decorate with config needed by auth routes
|
|
app.decorate("config", {
|
|
accessSecret: "test-jwt-secret-12345",
|
|
refreshSecret: "test-refresh-secret-12345",
|
|
accessTtl: 15,
|
|
refreshTtl: 7,
|
|
cookieOptions: { httpOnly: true, sameSite: "lax", secure: false, path: "/", maxAge: 15 * 60 },
|
|
refreshCookieOptions: { httpOnly: true, sameSite: "lax", secure: false, path: "/auth", maxAge: 7 * 24 * 60 * 60 },
|
|
});
|
|
|
|
await app.register(authRoutes);
|
|
await app.ready();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
testClient.close();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await clearData(testClient);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Auth State Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("GET /auth/state", () => {
|
|
it("should return auth state", async () => {
|
|
const response = await app.inject({
|
|
method: "GET",
|
|
url: "/auth/state",
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
const data = response.json();
|
|
expect(data.authEnabled).toBe(true);
|
|
expect(data.registrationEnabled).toBe(true);
|
|
expect(data.formLoginEnabled).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Registration Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("POST /auth/register", () => {
|
|
it("should register a new user", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "testuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(201);
|
|
const data = response.json();
|
|
expect(data.ok).toBe(true);
|
|
expect(data.user.username).toBe("testuser");
|
|
});
|
|
|
|
it("should reject duplicate username", async () => {
|
|
// First registration
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "duplicate",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
// Second registration with same username
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "duplicate",
|
|
password: "AnotherPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(409);
|
|
expect(response.json().code).toBe("USERNAME_EXISTS");
|
|
});
|
|
|
|
it("should reject duplicate username regardless of case", async () => {
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "CaseUser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "caseuser",
|
|
password: "AnotherPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(409);
|
|
expect(response.json().code).toBe("USERNAME_EXISTS");
|
|
});
|
|
|
|
it("should reject short password", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "testuser",
|
|
password: "short",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.json().code).toBe("FST_ERR_VALIDATION");
|
|
});
|
|
|
|
it("should reject short username", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "ab",
|
|
password: "ValidPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.json().code).toBe("FST_ERR_VALIDATION");
|
|
});
|
|
|
|
it("should register with trimmed username when input has whitespace", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: " trimuser ",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(201);
|
|
expect(response.json().user.username).toBe("trimuser");
|
|
});
|
|
|
|
it("should reject whitespace-only username on registration", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: " ",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.json().code).toBe("VALIDATION_ERROR");
|
|
});
|
|
|
|
it("should reject duplicate username even with surrounding whitespace", async () => {
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "spacedupe",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: " spacedupe ",
|
|
password: "AnotherPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(409);
|
|
expect(response.json().code).toBe("USERNAME_EXISTS");
|
|
});
|
|
|
|
it("should reject invalid username characters", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "test@user",
|
|
password: "ValidPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.json().code).toBe("VALIDATION_ERROR");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Login Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("POST /auth/login", () => {
|
|
beforeEach(async () => {
|
|
// Create a test user
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "loginuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should login with valid credentials", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "loginuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
const data = response.json();
|
|
expect(data.ok).toBe(true);
|
|
expect(data.user.username).toBe("loginuser");
|
|
|
|
// Should set cookies
|
|
const cookies = response.cookies;
|
|
expect(cookies.find((c: { name: string }) => c.name === "access_token")).toBeDefined();
|
|
expect(cookies.find((c: { name: string }) => c.name === "refresh_token")).toBeDefined();
|
|
});
|
|
|
|
it("should login case-insensitively with different username casing", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "LOGINUSER",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().ok).toBe(true);
|
|
expect(response.json().user.username).toBe("loginuser");
|
|
});
|
|
|
|
it("should reject invalid password", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "loginuser",
|
|
password: "WrongPassword",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
expect(response.json().code).toBe("INVALID_CREDENTIALS");
|
|
});
|
|
|
|
it("should reject non-existent user", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "nonexistent",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
expect(response.json().code).toBe("INVALID_CREDENTIALS");
|
|
});
|
|
|
|
it("should login successfully when username has leading/trailing whitespace", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: " loginuser ",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().ok).toBe(true);
|
|
expect(response.json().user.username).toBe("loginuser");
|
|
});
|
|
|
|
it("should reject whitespace-only username on login", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: " ",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.json().code).toBe("VALIDATION_ERROR");
|
|
});
|
|
|
|
it("should support rememberMe option", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "loginuser",
|
|
password: "TestPassword123",
|
|
rememberMe: true,
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
const data = response.json();
|
|
expect(data.ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Token Refresh Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("POST /auth/refresh", () => {
|
|
it("should refresh access token with valid refresh token", async () => {
|
|
// Login first to get tokens
|
|
const _loginResponse = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "loginuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
// Need to create user first
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "refreshuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const login = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "refreshuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const refreshToken = login.cookies.find((c: { name: string }) => c.name === "refresh_token");
|
|
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/refresh",
|
|
cookies: {
|
|
refresh_token: refreshToken?.value ?? "",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().ok).toBe(true);
|
|
});
|
|
|
|
it("should reject without refresh token", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/refresh",
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
expect(response.json().code).toBe("NO_REFRESH_TOKEN");
|
|
});
|
|
|
|
it("should reject invalid refresh token", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/refresh",
|
|
cookies: {
|
|
refresh_token: "invalid-token",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
expect(response.json().code).toBe("INVALID_REFRESH_TOKEN");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Logout Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("POST /auth/logout", () => {
|
|
it("should logout and clear cookies", async () => {
|
|
// Register and login first
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "logoutuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const login = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "logoutuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const refreshToken = login.cookies.find((c: { name: string }) => c.name === "refresh_token");
|
|
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/logout",
|
|
cookies: {
|
|
refresh_token: refreshToken?.value ?? "",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().ok).toBe(true);
|
|
});
|
|
|
|
it("should succeed even without refresh token", async () => {
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/logout",
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Me Endpoint Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("GET /auth/me", () => {
|
|
it("should return user info with valid access token", async () => {
|
|
// Register and login
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "meuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const login = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "meuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
|
|
|
const response = await app.inject({
|
|
method: "GET",
|
|
url: "/auth/me",
|
|
cookies: {
|
|
access_token: accessToken?.value ?? "",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
const data = response.json();
|
|
expect(data.username).toBe("meuser");
|
|
});
|
|
|
|
it("should reject without access token", async () => {
|
|
const response = await app.inject({
|
|
method: "GET",
|
|
url: "/auth/me",
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
});
|
|
|
|
it("should reject with invalid access token", async () => {
|
|
const response = await app.inject({
|
|
method: "GET",
|
|
url: "/auth/me",
|
|
cookies: {
|
|
access_token: "invalid.jwt.token",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Inactive User Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("Inactive user handling", () => {
|
|
it("should reject login for inactive user", async () => {
|
|
// Create user
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "inactiveuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
// Manually deactivate user in DB
|
|
await testClient.execute({
|
|
sql: "UPDATE users SET is_active = 0 WHERE username = ?",
|
|
args: ["inactiveuser"],
|
|
});
|
|
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "inactiveuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
expect(response.json().code).toBe("ACCOUNT_DISABLED");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Profile Update Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("PUT /auth/me (profile update)", () => {
|
|
it("should update password with valid current password", async () => {
|
|
// Register and login
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "profileuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const login = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "profileuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
|
|
|
const response = await app.inject({
|
|
method: "PUT",
|
|
url: "/auth/me",
|
|
cookies: {
|
|
access_token: accessToken?.value ?? "",
|
|
},
|
|
payload: {
|
|
currentPassword: "TestPassword123",
|
|
newPassword: "NewPassword456",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().ok).toBe(true);
|
|
|
|
// Verify can login with new password
|
|
const newLogin = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "profileuser",
|
|
password: "NewPassword456",
|
|
},
|
|
});
|
|
|
|
expect(newLogin.statusCode).toBe(200);
|
|
});
|
|
|
|
it("should reject password change without current password", async () => {
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "profileuser2",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const login = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "profileuser2",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
|
|
|
const response = await app.inject({
|
|
method: "PUT",
|
|
url: "/auth/me",
|
|
cookies: {
|
|
access_token: accessToken?.value ?? "",
|
|
},
|
|
payload: {
|
|
newPassword: "NewPassword456",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.json().code).toBe("CURRENT_PASSWORD_REQUIRED");
|
|
});
|
|
|
|
it("should reject password change with wrong current password", async () => {
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "profileuser3",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const login = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "profileuser3",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
|
|
|
const response = await app.inject({
|
|
method: "PUT",
|
|
url: "/auth/me",
|
|
cookies: {
|
|
access_token: accessToken?.value ?? "",
|
|
},
|
|
payload: {
|
|
currentPassword: "WrongPassword",
|
|
newPassword: "NewPassword456",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
expect(response.json().code).toBe("INVALID_PASSWORD");
|
|
});
|
|
|
|
it("should reject profile update without auth", async () => {
|
|
const response = await app.inject({
|
|
method: "PUT",
|
|
url: "/auth/me",
|
|
payload: {
|
|
currentPassword: "Test123",
|
|
newPassword: "NewPassword456",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
});
|
|
});
|
|
|
|
describe("DELETE /auth/me - Delete Account", () => {
|
|
it("should delete user account and all data", async () => {
|
|
// Register and login
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/auth/register",
|
|
payload: {
|
|
username: "deleteuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const login = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "deleteuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
|
|
|
// Delete account
|
|
const response = await app.inject({
|
|
method: "DELETE",
|
|
url: "/auth/me",
|
|
cookies: {
|
|
access_token: accessToken?.value ?? "",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().ok).toBe(true);
|
|
|
|
// Verify can't login anymore
|
|
const loginAgain = await app.inject({
|
|
method: "POST",
|
|
url: "/auth/login",
|
|
payload: {
|
|
username: "deleteuser",
|
|
password: "TestPassword123",
|
|
},
|
|
});
|
|
|
|
expect(loginAgain.statusCode).toBe(401);
|
|
});
|
|
|
|
it("should reject delete without auth", async () => {
|
|
const response = await app.inject({
|
|
method: "DELETE",
|
|
url: "/auth/me",
|
|
});
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
});
|
|
});
|
|
});
|