create-bun-stack
Version:
Rails-inspired fullstack application generator for Bun
447 lines (391 loc) • 14 kB
text/typescript
import { beforeAll, beforeEach, describe, expect, test } from "bun:test";
import { TEST_BASE_URL } from "../../helpers";
// Use real fetch
describe("CSRF Protection", () => {
beforeAll(async () => {
// Small delay to ensure server is ready
await new Promise((resolve) => setTimeout(resolve, 100));
});
beforeEach(async () => {
// Each test should use unique email addresses to avoid conflicts
// No direct database access in integration tests
});
describe("CSRF Token Generation", () => {
test("generates CSRF token on successful login", async () => {
// Create test user via API
const timestamp = Date.now();
await fetch(`${TEST_BASE_URL}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "CSRF Test User",
email: `csrf-${timestamp}@test.example.com`,
password: "password123",
}),
});
const response = await fetch(`${TEST_BASE_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: `csrf-${timestamp}@test.example.com`,
password: "password123",
}),
});
expect(response.status).toBe(200);
// Check for CSRF token in response
const data = await response.json();
expect(data.csrfToken).toBeDefined();
expect(data.csrfToken).toBeTypeOf("string");
expect(data.csrfToken.length).toBeGreaterThan(32);
// Check for CSRF cookie
const cookies = response.headers.get("Set-Cookie");
expect(cookies).toContain("csrf-token");
expect(cookies).toContain("HttpOnly");
expect(cookies).toContain("SameSite=Strict");
});
test("generates CSRF token on successful registration", async () => {
const timestamp = Date.now();
const response = await fetch(`${TEST_BASE_URL}/api/auth/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "New User",
email: `newuser-${timestamp}@test.example.com`,
password: "password123",
}),
});
expect(response.status).toBe(201);
const data = await response.json();
expect(data.csrfToken).toBeDefined();
const cookies = response.headers.get("Set-Cookie");
expect(cookies).toContain("csrf-token");
});
});
describe("CSRF Token Validation", () => {
let authToken: string;
let csrfToken: string;
let csrfCookie: string;
beforeEach(async () => {
// First register a user via API
const timestamp = Date.now();
const registerResponse = await fetch(`${TEST_BASE_URL}/api/auth/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Test User",
email: `validation-${timestamp}@test.example.com`,
password: "password123",
}),
});
if (!registerResponse.ok) {
throw new Error(`Registration failed: ${registerResponse.status}`);
}
// Then login to get tokens
const loginResponse = await fetch(`${TEST_BASE_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: `validation-${timestamp}@test.example.com`,
password: "password123",
}),
});
if (!loginResponse.ok) {
throw new Error(`Login failed: ${loginResponse.status}`);
}
const loginData = await loginResponse.json();
authToken = loginData.token;
csrfToken = loginData.csrfToken;
// Extract CSRF cookie
const cookies = loginResponse.headers.get("Set-Cookie");
const csrfCookieMatch = cookies?.match(/csrf-token=([^;]+)/);
csrfCookie = csrfCookieMatch?.[1] || "";
});
test("accepts valid CSRF token on POST requests", async () => {
const response = await fetch(`${TEST_BASE_URL}/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
"X-CSRF-Token": csrfToken,
Cookie: `csrf-token=${csrfCookie}`,
},
body: JSON.stringify({
name: "New User",
email: `newuser2-${Date.now()}@test.example.com`,
password: "password123",
}),
});
expect(response.status).toBe(201);
});
test("rejects POST requests without CSRF token", async () => {
const response = await fetch(`${TEST_BASE_URL}/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
Cookie: `csrf-token=${csrfCookie}`,
},
body: JSON.stringify({
name: "New User",
email: `newuser3-${Date.now()}@test.example.com`,
password: "password123",
}),
});
expect(response.status).toBe(403);
const data = await response.json();
expect(data.error).toContain("CSRF");
});
test("rejects POST requests with invalid CSRF token", async () => {
const response = await fetch(`${TEST_BASE_URL}/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
"X-CSRF-Token": "invalid-token",
Cookie: `csrf-token=${csrfCookie}`,
},
body: JSON.stringify({
name: "New User",
email: `newuser4-${Date.now()}@test.example.com`,
password: "password123",
}),
});
expect(response.status).toBe(403);
});
test("rejects POST requests with mismatched CSRF token and cookie", async () => {
const response = await fetch(`${TEST_BASE_URL}/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
"X-CSRF-Token": csrfToken,
Cookie: "csrf-token=wrong-cookie-value",
},
body: JSON.stringify({
name: "New User",
email: `newuser5-${Date.now()}@test.example.com`,
password: "password123",
}),
});
expect(response.status).toBe(403);
});
test("allows GET requests without CSRF token", async () => {
const response = await fetch(`${TEST_BASE_URL}/api/health`, {
headers: {
Authorization: `Bearer ${authToken}`,
},
});
expect(response.status).toBe(200);
});
test("requires CSRF for PUT requests", async () => {
// Create a user first via API
const createResponse = await fetch(`${TEST_BASE_URL}/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
"X-CSRF-Token": csrfToken,
Cookie: `csrf-token=${csrfCookie}`,
},
body: JSON.stringify({
name: "Update Test",
email: `update-${Date.now()}@test.example.com`,
password: "password123",
}),
});
const user = await createResponse.json();
const response = await fetch(`${TEST_BASE_URL}/api/users/${user.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
// Missing CSRF token
Cookie: `csrf-token=${csrfCookie}`,
},
body: JSON.stringify({
name: "Updated Name",
}),
});
expect(response.status).toBe(403);
});
test("requires CSRF for DELETE requests", async () => {
// Create a user first via API
const createResponse = await fetch(`${TEST_BASE_URL}/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
"X-CSRF-Token": csrfToken,
Cookie: `csrf-token=${csrfCookie}`,
},
body: JSON.stringify({
name: "Delete Test",
email: `delete-${Date.now()}@test.example.com`,
password: "password123",
}),
});
const user = await createResponse.json();
const response = await fetch(`${TEST_BASE_URL}/api/users/${user.id}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
// Missing CSRF token
Cookie: `csrf-token=${csrfCookie}`,
},
});
expect(response.status).toBe(403);
});
});
describe("CSRF Exemptions", () => {
test("login endpoint does not require CSRF token", async () => {
// Register user via API first
const timestamp = Date.now();
await fetch(`${TEST_BASE_URL}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Login Test",
email: `login-${timestamp}@test.example.com`,
password: "password123",
}),
});
const response = await fetch(`${TEST_BASE_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: `login-${timestamp}@test.example.com`,
password: "password123",
}),
});
expect(response.status).toBe(200);
});
test("register endpoint does not require CSRF token", async () => {
const response = await fetch(`${TEST_BASE_URL}/api/auth/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Register Test",
email: `register-${Date.now()}@test.example.com`,
password: "password123",
}),
});
expect(response.status).toBe(201);
});
test("health check does not require CSRF token", async () => {
const response = await fetch(`${TEST_BASE_URL}/api/health`);
expect(response.status).toBe(200);
});
});
describe("CSRF Token Lifecycle", () => {
test("CSRF token is cleared on logout", async () => {
// First register user via API
const timestamp = Date.now();
await fetch(`${TEST_BASE_URL}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Logout Test",
email: `logout-${timestamp}@test.example.com`,
password: "password123",
}),
});
const loginResponse = await fetch(`${TEST_BASE_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: `logout-${timestamp}@test.example.com`,
password: "password123",
}),
});
const { token, csrfToken } = await loginResponse.json();
const cookies = loginResponse.headers.get("Set-Cookie");
const csrfCookieMatch = cookies?.match(/csrf-token=([^;]+)/);
const csrfCookie = csrfCookieMatch?.[1] || "";
// Logout
const logoutResponse = await fetch(`${TEST_BASE_URL}/api/auth/logout`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"X-CSRF-Token": csrfToken,
Cookie: `csrf-token=${csrfCookie}`,
},
});
expect(logoutResponse.status).toBe(200);
// Check that CSRF cookie is cleared
const logoutCookies = logoutResponse.headers.get("Set-Cookie");
expect(logoutCookies).toContain("csrf-token=;");
expect(logoutCookies).toContain("Max-Age=0");
});
test("old CSRF tokens are invalidated after new login", async () => {
// Create user via API
const timestamp = Date.now();
await fetch(`${TEST_BASE_URL}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Token Rotation Test",
email: `rotation-${timestamp}@test.example.com`,
password: "password123",
}),
});
// First login
const firstLogin = await fetch(`${TEST_BASE_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: `rotation-${timestamp}@test.example.com`,
password: "password123",
}),
});
const firstData = await firstLogin.json();
const oldCsrfToken = firstData.csrfToken;
// Second login (should invalidate old token)
const secondLogin = await fetch(`${TEST_BASE_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: `rotation-${timestamp}@test.example.com`,
password: "password123",
}),
});
const secondData = await secondLogin.json();
const newCsrfToken = secondData.csrfToken;
// Tokens should be different
expect(newCsrfToken).not.toBe(oldCsrfToken);
// Try to use old CSRF token
const response = await fetch(`${TEST_BASE_URL}/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${firstData.token}`,
"X-CSRF-Token": oldCsrfToken,
Cookie: `csrf-token=${oldCsrfToken}`,
},
body: JSON.stringify({
name: "Should Fail",
email: `shouldfail-${Date.now()}@test.example.com`,
password: "password123",
}),
});
expect(response.status).toBe(403);
});
});
});