Skip to content

Commit 1150d0c

Browse files
update in codebase Done
1 parent 68c69c3 commit 1150d0c

6 files changed

Lines changed: 413 additions & 14 deletions

File tree

docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ services:
6767
SMTP_USER: ${SMTP_USER}
6868
SMTP_PASS: ${SMTP_PASS}
6969
EMAIL_FROM: ${EMAIL_FROM}
70+
MAIL_USER: ${MAIL_USER}
71+
MAIL_PASS: ${MAIL_PASS}
7072

7173
# ── External APIs ──
7274
REST_COUNTRIES_URL: ${REST_COUNTRIES_URL}
@@ -150,6 +152,8 @@ services:
150152
SMTP_USER: ${SMTP_USER}
151153
SMTP_PASS: ${SMTP_PASS}
152154
EMAIL_FROM: ${EMAIL_FROM}
155+
MAIL_USER: ${MAIL_USER}
156+
MAIL_PASS: ${MAIL_PASS}
153157

154158
# ── Redis Token TTLs ──
155159
EMAIL_VERIFICATION_EXPIRY: ${EMAIL_VERIFICATION_EXPIRY}

src/controllers/auth.controller.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,7 @@ export async function register(req, res) {
5252

5353
const result = await registerUser(req.body);
5454

55-
// The service only includes `otp` in development mode.
56-
// In production this object only has userId and email — never the OTP.
57-
const data = { userId: result.userId, email: result.email };
58-
if (result.otp) {
59-
data.otp = result.otp; // ⚠️ DEV ONLY — stripped automatically in production
60-
}
55+
const data = { userId: result.userId, email: result.email, otp: result.otp };
6156

6257
sendSuccess(res, {
6358
statusCode: HTTP.CREATED,

src/services/auth.service.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,14 +198,7 @@ export async function registerUser({ name, email, password }) {
198198
);
199199
});
200200

201-
// Only return the OTP in development so engineers can test without an inbox.
202-
// In production this field is simply absent from the response.
203-
const response = { userId: user._id, email: user.email };
204-
if (env.NODE_ENV !== 'production') {
205-
response.otp = otp;
206-
}
207-
208-
return response;
201+
return { userId: user._id, email: user.email, otp };
209202
}
210203

211204
// ─────────────────────────────────────────────────────────────────────────────
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Bug Condition Fix Validation — Controller-level
3+
*
4+
* Verifies that after the fix, the register controller always passes
5+
* otp: result.otp to sendSuccess unconditionally, regardless of environment.
6+
*
7+
* The mock returns { userId, email, otp: '048291' } — simulating the FIXED
8+
* service — and the test asserts that data.otp === '048291' in the sendSuccess
9+
* call, confirming the controller passes it through without any conditional guard.
10+
*
11+
* Validates: Requirements 1.1, 1.2
12+
*/
13+
14+
import { jest } from '@jest/globals';
15+
16+
// ─── Mock: apiResponse utility ────────────────────────────────────────────────
17+
let capturedSendSuccessArgs = null;
18+
jest.unstable_mockModule('../../../src/utils/apiResponse.js', () => ({
19+
sendSuccess: jest.fn().mockImplementation((res, args) => {
20+
capturedSendSuccessArgs = args;
21+
}),
22+
sendError: jest.fn(),
23+
}));
24+
25+
// ─── Mock: auth.service.js — simulates the FIXED service ─────────────────────
26+
// Returns { userId, email, otp } — the fixed service always includes otp
27+
jest.unstable_mockModule('../../../src/services/auth.service.js', () => ({
28+
registerUser: jest.fn().mockResolvedValue({
29+
userId: 'user123',
30+
email: 'alice@example.com',
31+
otp: '048291',
32+
}),
33+
verifyEmail: jest.fn(),
34+
resendVerificationEmail: jest.fn(),
35+
loginUser: jest.fn(),
36+
refreshAccessToken: jest.fn(),
37+
logoutUser: jest.fn(),
38+
forgotPassword: jest.fn(),
39+
resetPassword: jest.fn(),
40+
changePassword: jest.fn(),
41+
listRefreshTokens: jest.fn(),
42+
revokeRefreshToken: jest.fn(),
43+
revokeAllRefreshTokens: jest.fn(),
44+
buildRefreshCookieOptions: jest.fn().mockReturnValue({}),
45+
}));
46+
47+
// ─── Mock: enrichment service (used by controller) ───────────────────────────
48+
jest.unstable_mockModule('../../../src/services/enrichment.service.js', () => ({
49+
checkDisposableEmail: jest.fn().mockResolvedValue({ disposable: false }),
50+
getIPInfo: jest.fn().mockResolvedValue(null),
51+
}));
52+
53+
// ─── Mock: audit service (used by controller) ────────────────────────────────
54+
jest.unstable_mockModule('../../../src/services/audit.service.js', () => ({
55+
log: jest.fn().mockResolvedValue(undefined),
56+
}));
57+
58+
// ─── Import the controller AFTER all mocks are registered ────────────────────
59+
const { register } = await import('../../../src/controllers/auth.controller.js');
60+
61+
// ─────────────────────────────────────────────────────────────────────────────
62+
// Test Suite: Controller-level — OTP passed through to sendSuccess
63+
// ─────────────────────────────────────────────────────────────────────────────
64+
65+
describe('Fix Validation: Controller — OTP passed through to sendSuccess unconditionally', () => {
66+
beforeEach(() => {
67+
capturedSendSuccessArgs = null;
68+
jest.clearAllMocks();
69+
});
70+
71+
/**
72+
* After the fix, the controller uses `otp: result.otp` unconditionally
73+
* (no `if (result.otp)` guard). This test mocks registerUser to return
74+
* { userId, email, otp: '048291' } and asserts that data.otp === '048291'
75+
* in the sendSuccess call — confirming the controller passes it through.
76+
*
77+
* Validates: Requirements 1.1, 1.2
78+
*/
79+
it('data.otp passed to sendSuccess equals the otp returned by the service', async () => {
80+
const req = {
81+
body: {
82+
name: 'Alice',
83+
email: 'alice@example.com',
84+
password: 'Secret1!',
85+
},
86+
ip: '127.0.0.1',
87+
get: jest.fn().mockReturnValue('test-agent'),
88+
};
89+
90+
const res = {
91+
status: jest.fn().mockReturnThis(),
92+
json: jest.fn().mockReturnThis(),
93+
};
94+
95+
await register(req, res);
96+
97+
expect(capturedSendSuccessArgs).not.toBeNull();
98+
expect(capturedSendSuccessArgs.data).toBeDefined();
99+
expect(capturedSendSuccessArgs.data.otp).toBe('048291');
100+
});
101+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Bug Condition Fix Validation — Service-level
3+
*
4+
* Verifies that after the fix, registerUser always returns result.otp
5+
* regardless of NODE_ENV (including 'production').
6+
*
7+
* Validates: Requirements 1.1, 1.2
8+
*/
9+
10+
import { jest } from '@jest/globals';
11+
12+
// ─── Mock: env — force production environment ─────────────────────────────────
13+
jest.unstable_mockModule('../../../src/config/env.js', () => ({
14+
default: {
15+
NODE_ENV: 'production',
16+
OTP_EXPIRY: 600,
17+
PASSWORD_RESET_EXPIRY: 3600,
18+
PASETO_LOCAL_SECRET: 'test-secret',
19+
PASETO_ACCESS_EXPIRY: '15m',
20+
PASETO_REFRESH_EXPIRY: '7d',
21+
PASETO_REFRESH_COOKIE_MAX_AGE_MS: 604800000,
22+
},
23+
}));
24+
25+
// ─── Mock: User model ─────────────────────────────────────────────────────────
26+
// findOne must support .lean() chaining (auth.service.js calls findOne(...).lean())
27+
jest.unstable_mockModule('../../../src/models/User.model.js', () => ({
28+
default: {
29+
findOne: jest.fn().mockReturnValue({
30+
lean: jest.fn().mockResolvedValue(null), // no duplicate — registration proceeds
31+
}),
32+
create: jest.fn().mockResolvedValue({
33+
_id: 'user123',
34+
email: 'alice@example.com',
35+
}),
36+
},
37+
}));
38+
39+
// ─── Mock: Redis client ───────────────────────────────────────────────────────
40+
const mockRedisSet = jest.fn().mockResolvedValue('OK');
41+
const mockPipeline = jest.fn().mockReturnValue({
42+
set: jest.fn().mockReturnThis(),
43+
sadd: jest.fn().mockReturnThis(),
44+
expire: jest.fn().mockReturnThis(),
45+
exec: jest.fn().mockResolvedValue([]),
46+
});
47+
48+
jest.unstable_mockModule('../../../src/config/redis.js', () => ({
49+
getRedisClient: jest.fn().mockReturnValue({
50+
set: mockRedisSet,
51+
pipeline: mockPipeline,
52+
}),
53+
}));
54+
55+
// ─── Mock: Email service ──────────────────────────────────────────────────────
56+
jest.unstable_mockModule('../../../src/services/email.service.js', () => ({
57+
sendOtpEmail: jest.fn().mockResolvedValue(undefined),
58+
}));
59+
60+
// ─── Import the REAL service AFTER all mocks are registered ──────────────────
61+
const { registerUser } = await import('../../../src/services/auth.service.js');
62+
63+
// ─────────────────────────────────────────────────────────────────────────────
64+
// Test Suite: Service-level — OTP present in registerUser result in production
65+
// ─────────────────────────────────────────────────────────────────────────────
66+
67+
describe('Fix Validation: Service — OTP present in registerUser result in production', () => {
68+
/**
69+
* After the fix, registerUser must always return result.otp regardless of
70+
* NODE_ENV. This test runs with NODE_ENV === 'production' and asserts that
71+
* result.otp is defined and matches the 6-digit format.
72+
*
73+
* Validates: Requirements 1.1, 1.2
74+
*/
75+
it('result.otp is defined and matches /^\\d{6}$/ when NODE_ENV is production', async () => {
76+
const result = await registerUser({
77+
name: 'Alice',
78+
email: 'alice@example.com',
79+
password: 'Secret1!',
80+
});
81+
82+
expect(result.otp).toBeDefined();
83+
expect(result.otp).toMatch(/^\d{6}$/);
84+
});
85+
});

0 commit comments

Comments
 (0)