Compare commits

...

2 Commits

Author SHA1 Message Date
jackiettran
2ee4b5c389 fixed map tests 2025-11-06 16:56:17 -05:00
jackiettran
2956b79f34 updated test 2025-11-06 16:28:35 -05:00
4 changed files with 163 additions and 66 deletions

1
.gitignore vendored
View File

@@ -18,6 +18,7 @@ node_modules/
.env.test.local
.env.production.local
.mcp.json
.claude
# Logs
npm-debug.log*

View File

@@ -1,7 +1,22 @@
const crypto = require('crypto');
// Mock crypto module
jest.mock('crypto');
jest.mock('crypto', () => ({
randomBytes: jest.fn(),
createHash: jest.fn(() => ({
update: jest.fn().mockReturnThis(),
digest: jest.fn((encoding) => {
if (encoding === 'hex') {
return 'a'.repeat(64); // Deterministic hash for testing
}
return Buffer.from('a'.repeat(32));
})
})),
timingSafeEqual: jest.fn((a, b) => {
if (a.length !== b.length) return false;
return a.equals(b);
})
}));
// Mock the entire models module
jest.mock('../../../models', () => {
@@ -39,6 +54,7 @@ describe('User Model - Password Reset', () => {
password: 'hashedPassword123',
passwordResetToken: null,
passwordResetTokenExpiry: null,
jwtVersion: 0,
update: jest.fn().mockImplementation(function(updates) {
Object.assign(this, updates);
return Promise.resolve(this);
@@ -52,7 +68,7 @@ describe('User Model - Password Reset', () => {
describe('generatePasswordResetToken', () => {
it('should generate a random token and set 1-hour expiry', async () => {
const mockRandomBytes = Buffer.from('a'.repeat(32));
const mockToken = mockRandomBytes.toString('hex');
const hashedToken = 'a'.repeat(64); // Hashed version stored in DB
crypto.randomBytes.mockReturnValue(mockRandomBytes);
@@ -61,7 +77,7 @@ describe('User Model - Password Reset', () => {
expect(crypto.randomBytes).toHaveBeenCalledWith(32);
expect(mockUser.update).toHaveBeenCalledWith(
expect.objectContaining({
passwordResetToken: mockToken
passwordResetToken: hashedToken // Expect hashed token in DB
})
);
@@ -76,44 +92,48 @@ describe('User Model - Password Reset', () => {
it('should update the user with token and expiry', async () => {
const mockRandomBytes = Buffer.from('b'.repeat(32));
const mockToken = mockRandomBytes.toString('hex');
const plainToken = mockRandomBytes.toString('hex');
crypto.randomBytes.mockReturnValue(mockRandomBytes);
const result = await User.prototype.generatePasswordResetToken.call(mockUser);
expect(mockUser.update).toHaveBeenCalledTimes(1);
expect(result.passwordResetToken).toBe(mockToken);
expect(result.passwordResetTokenExpiry).toBeInstanceOf(Date);
expect(result).toBe(plainToken); // Method returns plain token
expect(mockUser.passwordResetToken).toBe('a'.repeat(64)); // DB has hashed token
expect(mockUser.passwordResetTokenExpiry).toBeInstanceOf(Date);
});
it('should generate unique tokens on multiple calls', async () => {
const mockRandomBytes1 = Buffer.from('a'.repeat(32));
const mockRandomBytes2 = Buffer.from('b'.repeat(32));
const plainToken1 = mockRandomBytes1.toString('hex');
const plainToken2 = mockRandomBytes2.toString('hex');
crypto.randomBytes
.mockReturnValueOnce(mockRandomBytes1)
.mockReturnValueOnce(mockRandomBytes2);
await User.prototype.generatePasswordResetToken.call(mockUser);
const firstToken = mockUser.update.mock.calls[0][0].passwordResetToken;
const result1 = await User.prototype.generatePasswordResetToken.call(mockUser);
const result2 = await User.prototype.generatePasswordResetToken.call(mockUser);
await User.prototype.generatePasswordResetToken.call(mockUser);
const secondToken = mockUser.update.mock.calls[1][0].passwordResetToken;
expect(firstToken).not.toBe(secondToken);
// Plain tokens returned should be different
expect(result1).toBe(plainToken1);
expect(result2).toBe(plainToken2);
expect(plainToken1).not.toBe(plainToken2);
});
});
describe('isPasswordResetTokenValid', () => {
it('should return true for valid token and non-expired time', () => {
const validToken = 'valid-token-123';
const plainToken = 'valid-token-123';
const hashedToken = 'a'.repeat(64); // Mocked hash result
const futureExpiry = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes from now
mockUser.passwordResetToken = validToken;
mockUser.passwordResetToken = hashedToken; // Store hashed token
mockUser.passwordResetTokenExpiry = futureExpiry;
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken);
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);
expect(result).toBe(true);
});
@@ -128,7 +148,8 @@ describe('User Model - Password Reset', () => {
});
it('should return false for missing expiry', () => {
mockUser.passwordResetToken = 'valid-token';
const hashedToken = 'a'.repeat(64);
mockUser.passwordResetToken = hashedToken;
mockUser.passwordResetTokenExpiry = null;
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, 'valid-token');
@@ -137,60 +158,71 @@ describe('User Model - Password Reset', () => {
});
it('should return false for mismatched token', () => {
mockUser.passwordResetToken = 'correct-token';
const hashedToken = 'a'.repeat(64);
mockUser.passwordResetToken = hashedToken;
mockUser.passwordResetTokenExpiry = new Date(Date.now() + 30 * 60 * 1000);
// Mock createHash to return different hash for wrong token
crypto.createHash.mockReturnValueOnce({
update: jest.fn().mockReturnThis(),
digest: jest.fn(() => 'b'.repeat(64)) // Different hash
});
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, 'wrong-token');
expect(result).toBe(false);
});
it('should return false for expired token', () => {
const validToken = 'valid-token-123';
const plainToken = 'valid-token-123';
const hashedToken = 'a'.repeat(64);
const pastExpiry = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago
mockUser.passwordResetToken = validToken;
mockUser.passwordResetToken = hashedToken;
mockUser.passwordResetTokenExpiry = pastExpiry;
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken);
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);
expect(result).toBe(false);
});
it('should return false for token expiring in the past by 1 second', () => {
const validToken = 'valid-token-123';
const plainToken = 'valid-token-123';
const hashedToken = 'a'.repeat(64);
const pastExpiry = new Date(Date.now() - 1000); // 1 second ago
mockUser.passwordResetToken = validToken;
mockUser.passwordResetToken = hashedToken;
mockUser.passwordResetTokenExpiry = pastExpiry;
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken);
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);
expect(result).toBe(false);
});
it('should handle edge case of token expiring exactly now', () => {
const validToken = 'valid-token-123';
const plainToken = 'valid-token-123';
const hashedToken = 'a'.repeat(64);
// Set expiry 1ms in the future to handle timing precision
const nowExpiry = new Date(Date.now() + 1);
mockUser.passwordResetToken = validToken;
mockUser.passwordResetToken = hashedToken;
mockUser.passwordResetTokenExpiry = nowExpiry;
// This should be true because expiry is slightly in the future
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken);
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);
expect(result).toBe(true);
});
it('should handle string dates correctly', () => {
const validToken = 'valid-token-123';
const plainToken = 'valid-token-123';
const hashedToken = 'a'.repeat(64);
const futureExpiry = new Date(Date.now() + 30 * 60 * 1000).toISOString(); // String date
mockUser.passwordResetToken = validToken;
mockUser.passwordResetToken = hashedToken;
mockUser.passwordResetTokenExpiry = futureExpiry;
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken);
const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);
expect(result).toBe(true);
});
@@ -200,22 +232,26 @@ describe('User Model - Password Reset', () => {
it('should update password and clear token fields', async () => {
mockUser.passwordResetToken = 'some-token';
mockUser.passwordResetTokenExpiry = new Date();
mockUser.jwtVersion = 0;
await User.prototype.resetPassword.call(mockUser, 'newSecurePassword123!');
expect(mockUser.update).toHaveBeenCalledWith({
password: 'newSecurePassword123!',
passwordResetToken: null,
passwordResetTokenExpiry: null
passwordResetTokenExpiry: null,
jwtVersion: 1
});
});
it('should return updated user object', async () => {
mockUser.jwtVersion = 0;
const result = await User.prototype.resetPassword.call(mockUser, 'newPassword123!');
expect(result.password).toBe('newPassword123!');
expect(result.passwordResetToken).toBe(null);
expect(result.passwordResetTokenExpiry).toBe(null);
expect(result.jwtVersion).toBe(1);
});
it('should call update only once', async () => {
@@ -229,34 +265,45 @@ describe('User Model - Password Reset', () => {
it('should complete full password reset flow successfully', async () => {
// Step 1: Generate password reset token
const mockRandomBytes = Buffer.from('c'.repeat(32));
const mockToken = mockRandomBytes.toString('hex');
const plainToken = mockRandomBytes.toString('hex');
const hashedToken = 'a'.repeat(64);
crypto.randomBytes.mockReturnValue(mockRandomBytes);
await User.prototype.generatePasswordResetToken.call(mockUser);
const returnedToken = await User.prototype.generatePasswordResetToken.call(mockUser);
expect(mockUser.passwordResetToken).toBe(mockToken);
expect(returnedToken).toBe(plainToken); // Returns plain token
expect(mockUser.passwordResetToken).toBe(hashedToken); // Stores hashed token
expect(mockUser.passwordResetTokenExpiry).toBeInstanceOf(Date);
// Step 2: Validate token
const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, mockToken);
// Step 2: Validate token (pass plain token, compares with hashed)
const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);
expect(isValid).toBe(true);
// Step 3: Reset password
mockUser.jwtVersion = 0;
await User.prototype.resetPassword.call(mockUser, 'newPassword123!');
expect(mockUser.password).toBe('newPassword123!');
expect(mockUser.passwordResetToken).toBe(null);
expect(mockUser.passwordResetTokenExpiry).toBe(null);
expect(mockUser.jwtVersion).toBe(1);
});
it('should fail password reset with wrong token', async () => {
// Generate token
const mockToken = 'd'.repeat(64);
const mockRandomBytes = Buffer.from('d'.repeat(32));
const plainToken = mockRandomBytes.toString('hex');
const hashedToken = 'a'.repeat(64);
crypto.randomBytes.mockReturnValue(mockRandomBytes);
await User.prototype.generatePasswordResetToken.call(mockUser);
// Mock createHash to return different hash for wrong token
crypto.createHash.mockReturnValueOnce({
update: jest.fn().mockReturnThis(),
digest: jest.fn(() => 'b'.repeat(64)) // Different hash
});
// Try to validate with wrong token
const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, 'wrong-token');
@@ -265,7 +312,8 @@ describe('User Model - Password Reset', () => {
it('should fail password reset with expired token', async () => {
// Manually set an expired token
mockUser.passwordResetToken = 'expired-token';
const hashedToken = 'a'.repeat(64);
mockUser.passwordResetToken = hashedToken;
mockUser.passwordResetTokenExpiry = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago
const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, 'expired-token');
@@ -276,16 +324,17 @@ describe('User Model - Password Reset', () => {
it('should not allow password reset after token has been used', async () => {
// Generate token
const mockRandomBytes = Buffer.from('e'.repeat(32));
const mockToken = mockRandomBytes.toString('hex');
const plainToken = mockRandomBytes.toString('hex');
crypto.randomBytes.mockReturnValue(mockRandomBytes);
await User.prototype.generatePasswordResetToken.call(mockUser);
// Reset password (clears token)
mockUser.jwtVersion = 0;
await User.prototype.resetPassword.call(mockUser, 'newPassword123!');
// Try to validate same token again (should fail because it's cleared)
const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, mockToken);
const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);
expect(isValid).toBe(false);
});

View File

@@ -1,6 +1,21 @@
const request = require('supertest');
const express = require('express');
// Mock logger
const mockLogger = {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
http: jest.fn(),
withRequestId: jest.fn()
};
// Set up withRequestId to return the same mock logger
mockLogger.withRequestId.mockReturnValue(mockLogger);
jest.mock('../../../utils/logger', () => mockLogger);
// Mock dependencies
jest.mock('../../../services/googleMapsService', () => ({
getPlacesAutocomplete: jest.fn(),
@@ -136,9 +151,11 @@ describe('Maps Routes', () => {
error: 'Maps service temporarily unavailable',
details: 'Configuration issue'
});
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Maps service error:',
'API key not configured'
expect(mockLogger.error).toHaveBeenCalledWith(
'Maps service error',
expect.objectContaining({
error: 'API key not configured'
})
);
});
@@ -216,8 +233,13 @@ describe('Maps Routes', () => {
}
);
expect(consoleLogSpy).toHaveBeenCalledWith(
'Places Autocomplete: user=1, query_length=8, results=2'
expect(mockLogger.info).toHaveBeenCalledWith(
'Places Autocomplete request',
expect.objectContaining({
userId: 1,
queryLength: 8,
resultsCount: 2
})
);
});
@@ -281,8 +303,11 @@ describe('Maps Routes', () => {
expect(response.status).toBe(200);
// Should log with user ID
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('user=1')
expect(mockLogger.info).toHaveBeenCalledWith(
'Places Autocomplete request',
expect.objectContaining({
userId: 1
})
);
});
@@ -297,8 +322,13 @@ describe('Maps Routes', () => {
expect(response.status).toBe(200);
expect(response.body).toEqual({ predictions: [] });
expect(consoleLogSpy).toHaveBeenCalledWith(
'Places Autocomplete: user=1, query_length=17, results=0'
expect(mockLogger.info).toHaveBeenCalledWith(
'Places Autocomplete request',
expect.objectContaining({
userId: 1,
queryLength: 17,
resultsCount: 0
})
);
});
@@ -313,8 +343,13 @@ describe('Maps Routes', () => {
expect(response.status).toBe(200);
expect(response.body).toEqual({});
expect(consoleLogSpy).toHaveBeenCalledWith(
'Places Autocomplete: user=1, query_length=4, results=0'
expect(mockLogger.info).toHaveBeenCalledWith(
'Places Autocomplete request',
expect.objectContaining({
userId: 1,
queryLength: 4,
resultsCount: 0
})
);
});
});
@@ -351,8 +386,12 @@ describe('Maps Routes', () => {
{ sessionToken: 'session123' }
);
expect(consoleLogSpy).toHaveBeenCalledWith(
'Place Details: user=1, placeId=ChIJ123abc...'
expect(mockLogger.info).toHaveBeenCalledWith(
'Place Details request',
expect.objectContaining({
userId: 1,
placeIdPrefix: 'ChIJ123abc...'
})
);
});
@@ -410,8 +449,12 @@ describe('Maps Routes', () => {
.send({ placeId: longPlaceId });
expect(response.status).toBe(200);
expect(consoleLogSpy).toHaveBeenCalledWith(
`Place Details: user=1, placeId=${longPlaceId.substring(0, 10)}...`
expect(mockLogger.info).toHaveBeenCalledWith(
'Place Details request',
expect.objectContaining({
userId: 1,
placeIdPrefix: longPlaceId.substring(0, 10) + '...'
})
);
});
@@ -465,8 +508,12 @@ describe('Maps Routes', () => {
{ componentRestrictions: { country: 'US' } }
);
expect(consoleLogSpy).toHaveBeenCalledWith(
'Geocoding: user=1, address_length=25'
expect(mockLogger.info).toHaveBeenCalledWith(
'Geocoding request',
expect.objectContaining({
userId: 1,
addressLength: 25
})
);
});

View File

@@ -287,12 +287,7 @@ const Owning: React.FC = () => {
return (
<div className="container mt-4">
<div className="d-flex justify-content-between align-items-center mb-4">
<h1>Owning</h1>
<Link to="/create-item" className="btn btn-primary">
Add New Item
</Link>
</div>
<h1 className="mb-4">Owning</h1>
{error && (
<div className="alert alert-danger" role="alert">
@@ -487,10 +482,15 @@ const Owning: React.FC = () => {
</div>
)}
<h4 className="mb-3">
<div className="d-flex justify-content-between align-items-center mb-3">
<h4 className="mb-0">
<i className="bi bi-list-ul me-2"></i>
Listings
</h4>
<Link to="/create-item" className="btn btn-primary">
Add New Item
</Link>
</div>
{listings.length === 0 ? (
<div className="text-center py-5">