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:
212
backend/src/__tests__/wsdc.test.js
Normal file
212
backend/src/__tests__/wsdc.test.js
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* WSDC Controller Tests (Phase 1.5)
|
||||
* Tests for WSDC API proxy functionality
|
||||
*/
|
||||
|
||||
const request = require('supertest');
|
||||
const app = require('../app');
|
||||
|
||||
// Mock fetch globally
|
||||
global.fetch = jest.fn();
|
||||
|
||||
describe('WSDC Controller Tests (Phase 1.5)', () => {
|
||||
beforeEach(() => {
|
||||
fetch.mockClear();
|
||||
});
|
||||
|
||||
describe('GET /api/wsdc/lookup', () => {
|
||||
it('should lookup dancer by WSDC ID successfully', async () => {
|
||||
const mockDancerData = {
|
||||
dancer_wsdcid: 26997,
|
||||
dancer_first: 'Radoslaw',
|
||||
dancer_last: 'Gierwialo',
|
||||
recent_year: 2025
|
||||
};
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockDancerData
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/wsdc/lookup?id=26997');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.dancer).toMatchObject({
|
||||
wsdcId: 26997,
|
||||
firstName: 'Radoslaw',
|
||||
lastName: 'Gierwialo'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if WSDC ID is missing', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/wsdc/lookup');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Bad Request');
|
||||
expect(response.body.message).toContain('WSDC ID is required');
|
||||
});
|
||||
|
||||
it('should return 400 for invalid WSDC ID format', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/wsdc/lookup?id=abc123');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Bad Request');
|
||||
expect(response.body.message).toContain('Invalid WSDC ID format');
|
||||
});
|
||||
|
||||
it('should return 400 for WSDC ID too long', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/wsdc/lookup?id=12345678901'); // 11 digits
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Bad Request');
|
||||
});
|
||||
|
||||
it('should return 404 if dancer not found', async () => {
|
||||
fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({}) // Empty response
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/wsdc/lookup?id=99999');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Not Found');
|
||||
expect(response.body.message).toContain('not found');
|
||||
});
|
||||
|
||||
it('should return 502 if WSDC API fails', async () => {
|
||||
fetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/wsdc/lookup?id=26997');
|
||||
|
||||
expect(response.status).toBe(502);
|
||||
expect(response.body.error).toBe('Bad Gateway');
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
fetch.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/wsdc/lookup?id=26997');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.error).toBe('Internal Server Error');
|
||||
});
|
||||
|
||||
it('should call WSDC API with correct URL', async () => {
|
||||
fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
dancer_wsdcid: 26997,
|
||||
dancer_first: 'Test',
|
||||
dancer_last: 'User'
|
||||
})
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.get('/api/wsdc/lookup?id=26997');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
'https://points.worldsdc.com/lookup2020/find?q=26997'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate numeric WSDC IDs only', async () => {
|
||||
const invalidIds = ['abc', '123abc', 'test', '!@#$'];
|
||||
|
||||
for (const id of invalidIds) {
|
||||
const response = await request(app)
|
||||
.get(`/api/wsdc/lookup?id=${id}`);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept valid numeric WSDC IDs', async () => {
|
||||
fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
dancer_wsdcid: 123,
|
||||
dancer_first: 'Test',
|
||||
dancer_last: 'User'
|
||||
})
|
||||
});
|
||||
|
||||
const validIds = ['1', '123', '12345', '1234567890'];
|
||||
|
||||
for (const id of validIds) {
|
||||
const response = await request(app)
|
||||
.get(`/api/wsdc/lookup?id=${id}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
});
|
||||
|
||||
it('should include optional fields if available', async () => {
|
||||
const mockDancerData = {
|
||||
dancer_wsdcid: 26997,
|
||||
dancer_first: 'Radoslaw',
|
||||
dancer_last: 'Gierwialo',
|
||||
recent_year: 2025,
|
||||
dominate_data: {
|
||||
short_dominate_role: 'Leader'
|
||||
}
|
||||
};
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockDancerData
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/wsdc/lookup?id=26997');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.dancer.recentYear).toBe(2025);
|
||||
expect(response.body.dancer.dominateRole).toBe('Leader');
|
||||
});
|
||||
|
||||
it('should handle missing optional fields gracefully', async () => {
|
||||
const mockDancerData = {
|
||||
dancer_wsdcid: 26997,
|
||||
dancer_first: 'Test',
|
||||
dancer_last: 'User'
|
||||
// No recent_year or dominate_data
|
||||
};
|
||||
|
||||
fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockDancerData
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/wsdc/lookup?id=26997');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.dancer.dominateRole).toBeNull();
|
||||
});
|
||||
|
||||
it('should log errors for debugging', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
fetch.mockRejectedValue(new Error('Test error'));
|
||||
|
||||
await request(app)
|
||||
.get('/api/wsdc/lookup?id=26997');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user