feat: add email verification, password reset, and WSDC integration (Phase 1.5)
Backend features: - AWS SES email service with HTML templates - Email verification with dual method (link + 6-digit PIN code) - Password reset workflow with secure tokens - WSDC API proxy for dancer lookup and auto-fill registration - Extended User model with verification and WSDC fields - Email verification middleware for protected routes Frontend features: - Two-step registration with WSDC ID lookup - Password strength indicator component - Email verification page with code input - Password reset flow (request + reset pages) - Verification banner for unverified users - Updated authentication context and API service Testing: - 65 unit tests with 100% coverage of new features - Tests for auth utils, email service, WSDC controller, and middleware - Integration tests for full authentication flows - Comprehensive mocking of AWS SES and external APIs Database: - Migration: add WSDC fields (firstName, lastName, wsdcId) - Migration: add email verification fields (token, code, expiry) - Migration: add password reset fields (token, expiry) Documentation: - Complete Phase 1.5 documentation - Test suite documentation and best practices - Updated session context with new features
This commit is contained in:
188
backend/src/__tests__/middleware/auth.test.js
Normal file
188
backend/src/__tests__/middleware/auth.test.js
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Auth Middleware Tests (Phase 1.5)
|
||||
* Tests for authentication and email verification middleware
|
||||
*/
|
||||
|
||||
const { requireEmailVerification } = require('../../middleware/auth');
|
||||
|
||||
describe('Auth Middleware Tests (Phase 1.5)', () => {
|
||||
let req, res, next;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
user: null
|
||||
};
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn()
|
||||
};
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
describe('requireEmailVerification', () => {
|
||||
it('should pass through if email is verified', async () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'user@example.com',
|
||||
emailVerified: true
|
||||
};
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 401 if user is not attached to request', async () => {
|
||||
req.user = null;
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: 'Unauthorized'
|
||||
})
|
||||
);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 403 if email is not verified', async () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'user@example.com',
|
||||
emailVerified: false
|
||||
};
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: 'Email Not Verified',
|
||||
requiresVerification: true
|
||||
})
|
||||
);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should include helpful message for unverified email', async () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'user@example.com',
|
||||
emailVerified: false
|
||||
};
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('verify your email')
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle undefined emailVerified as false', async () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'user@example.com'
|
||||
// emailVerified is undefined
|
||||
};
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
// Force an error by making req.user a getter that throws
|
||||
Object.defineProperty(req, 'user', {
|
||||
get: () => {
|
||||
throw new Error('Test error');
|
||||
}
|
||||
});
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: 'Internal Server Error'
|
||||
})
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not call next if verification fails', async () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'user@example.com',
|
||||
emailVerified: false
|
||||
};
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should work with verified users multiple times', async () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'user@example.com',
|
||||
emailVerified: true
|
||||
};
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
await requireEmailVerification(req, res, next);
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should handle boolean true emailVerified', async () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'user@example.com',
|
||||
emailVerified: true
|
||||
};
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle boolean false emailVerified', async () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'user@example.com',
|
||||
emailVerified: false
|
||||
};
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should include requiresVerification flag in response', async () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'user@example.com',
|
||||
emailVerified: false
|
||||
};
|
||||
|
||||
await requireEmailVerification(req, res, next);
|
||||
|
||||
const jsonCall = res.json.mock.calls[0][0];
|
||||
expect(jsonCall.requiresVerification).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user