import { describe, it, expect, vi, beforeEach } from "vitest"; import crypto from "node:crypto"; // Mock crypto to test both real and mocked behavior const originalRandomBytes = crypto.randomBytes; describe("Password Reset Token Security", () => { beforeEach(() => { // Restore original crypto function for these tests crypto.randomBytes = originalRandomBytes; }); describe("Token Generation Security Properties", () => { it("should generate tokens with 64 characters (32 bytes as hex)", () => { const token = crypto.randomBytes(32).toString("hex"); expect(token).toHaveLength(64); }); it("should generate unique tokens on each call", () => { const token1 = crypto.randomBytes(32).toString("hex"); const token2 = crypto.randomBytes(32).toString("hex"); const token3 = crypto.randomBytes(32).toString("hex"); expect(token1).not.toBe(token2); expect(token2).not.toBe(token3); expect(token1).not.toBe(token3); }); it("should generate tokens with proper entropy (no obvious patterns)", () => { const tokens = new Set(); const numTokens = 100; // Generate multiple tokens to check for patterns for (let i = 0; i < numTokens; i++) { const token = crypto.randomBytes(32).toString("hex"); tokens.add(token); } // All tokens should be unique expect(tokens.size).toBe(numTokens); }); it("should generate tokens with hex characters only", () => { const token = crypto.randomBytes(32).toString("hex"); const hexPattern = /^[0-9a-f]+$/; expect(token).toMatch(hexPattern); }); it("should have sufficient entropy to prevent brute force attacks", () => { // 32 bytes = 256 bits of entropy // This provides 2^256 possible combinations const token = crypto.randomBytes(32).toString("hex"); // Verify we have the expected length for 256-bit security expect(token).toHaveLength(64); // Verify character distribution is roughly uniform const charCounts = {}; for (const char of token) { charCounts[char] = (charCounts[char] || 0) + 1; } // Should have at least some variety in characters expect(Object.keys(charCounts).length).toBeGreaterThan(5); }); it("should be significantly more secure than Math.random() approach", () => { // Generate tokens using both methods for comparison const secureToken = crypto.randomBytes(32).toString("hex"); const weakToken = Math.random().toString(36).substring(2, 15); // Secure token should be much longer expect(secureToken.length).toBeGreaterThan(weakToken.length * 4); // Secure token has proper hex format expect(secureToken).toMatch(/^[0-9a-f]{64}$/); // Weak token has predictable format expect(weakToken).toMatch(/^[0-9a-z]+$/); expect(weakToken.length).toBeLessThan(14); }); }); describe("Token Collision Resistance", () => { it("should have virtually zero probability of collision", () => { const tokens = new Set(); const iterations = 10000; // Generate many tokens to test collision resistance for (let i = 0; i < iterations; i++) { const token = crypto.randomBytes(32).toString("hex"); expect(tokens.has(token)).toBe(false); // No collisions tokens.add(token); } expect(tokens.size).toBe(iterations); }); }); describe("Performance Characteristics", () => { it("should generate tokens in reasonable time", () => { const startTime = Date.now(); // Generate 1000 tokens for (let i = 0; i < 1000; i++) { crypto.randomBytes(32).toString("hex"); } const endTime = Date.now(); const duration = endTime - startTime; // Should complete in under 1 second expect(duration).toBeLessThan(1000); }); }); describe("Token Format Validation", () => { it("should always produce lowercase hex", () => { for (let i = 0; i < 10; i++) { const token = crypto.randomBytes(32).toString("hex"); expect(token).toBe(token.toLowerCase()); expect(token).toMatch(/^[0-9a-f]{64}$/); } }); it("should never produce tokens starting with predictable patterns", () => { const tokens = []; for (let i = 0; i < 100; i++) { tokens.push(crypto.randomBytes(32).toString("hex")); } // Check that tokens don't all start with same character const firstChars = new Set(tokens.map((t) => t[0])); expect(firstChars.size).toBeGreaterThan(1); // Check that we don't have obvious patterns like all starting with '0' const zeroStart = tokens.filter((t) => t.startsWith("0")).length; expect(zeroStart).toBeLessThan(tokens.length * 0.8); // Should be roughly 1/16 }); }); });