/** * Property-Based Test: Password Change Round-Trip * * Feature: user-profile, Property 3: Password change round-trip * * For any valid current password and any new password of 8+ characters, * after a successful change, bcrypt.compare(newPassword, storedHash) returns true. * * Validates: Requirements 2.2, 2.7 */ const fc = require('fast-check'); const bcrypt = require('bcryptjs'); // bcrypt cost factor — production uses 10, but we use 4 (the minimum) here // to keep 100 iterations feasible within test timeouts. The round-trip property // holds regardless of cost factor. const BCRYPT_COST = 4; describe('Feature: user-profile, Property 3: Password change round-trip', () => { it('after a password change, bcrypt.compare(newPassword, newHash) returns true', async () => { await fc.assert( fc.asyncProperty( // Current password: any non-empty string (length >= 1) fc.string({ minLength: 1, maxLength: 72 }), // New password: any string of length >= 8 (bcrypt max input is 72 bytes) fc.string({ minLength: 8, maxLength: 72 }), async (currentPassword, newPassword) => { // Step 1: Hash the current password (simulates existing stored hash) const currentHash = await bcrypt.hash(currentPassword, BCRYPT_COST); // Step 2: Verify the current password against the stored hash // (simulates the bcrypt.compare check in the change-password route) const currentPasswordValid = await bcrypt.compare(currentPassword, currentHash); expect(currentPasswordValid).toBe(true); // Step 3: Hash the new password (simulates bcrypt.hash(newPassword, 10) in the route) const newHash = await bcrypt.hash(newPassword, BCRYPT_COST); // Step 4: Verify the new password matches the new hash (round-trip property) const newPasswordValid = await bcrypt.compare(newPassword, newHash); expect(newPasswordValid).toBe(true); } ), { numRuns: 100 } ); }, 120000); // 2-minute timeout for 100 bcrypt iterations });