'use strict'; /** * Integration tests – user controller (controllers/user.js) */ const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); const { mockApplicator, mockReq, mockRes, newId } = require('./mock_data'); let User, Customer; beforeAll(async () => { await connectDB(); User = require('../../model/user'); Customer = require('../../model/customer'); }); afterAll(async () => { await disconnectDB(); }); const userCtl = require('../../controllers/user'); const { sign } = require('../../helpers/jwt_async'); describe('user controller – data methods', () => { let applicator; beforeAll(async () => { applicator = await Customer.create(mockApplicator()); }); afterAll(async () => { await Customer.deleteMany({ _id: applicator._id }); }); const makeReq = (extra = {}) => mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); // Add app and hostname to req for methods that call getHostUrlFromReq const makeReqWithApp = (extra = {}) => { const req = makeReq(extra); req.app = { isProd: false }; req.hostname = 'localhost'; return req; }; // ------------------------------------------------------------------------- describe('getUser_get', () => { it('returns a user by id', async () => { const req = makeReq({ params: { id: String(applicator._id) }, query: {} }); const res = mockRes(); await userCtl.getUser_get(req, res); expect(res.json).toHaveBeenCalled(); expect(String(res._data._id)).toBe(String(applicator._id)); }); it('returns null when user not found', async () => { const req = makeReq({ params: { id: String(newId()) }, query: {} }); const res = mockRes(); await userCtl.getUser_get(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data).toBeNull(); }); }); // ------------------------------------------------------------------------- describe('updateUser_put', () => { it('updates a user field', async () => { const req = makeReq({ params: { id: String(applicator._id) }, body: { name: 'Updated Applicator Name', kind: '1' }, }); const res = mockRes(); await userCtl.updateUser_put(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data.name).toBe('Updated Applicator Name'); }); }); // ------------------------------------------------------------------------- describe('usernameExists_post', () => { it('returns true when username exists', async () => { const req = makeReq({ body: { username: applicator.username } }); const res = mockRes(); await userCtl.isUserNameExists_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data).toBe(1); }); it('returns false when username does not exist', async () => { const req = makeReq({ body: { username: `nonexistent_${Date.now()}@test.com` } }); const res = mockRes(); await userCtl.isUserNameExists_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data).toBe(0); }); }); // ------------------------------------------------------------------------- describe('searchUsers_post', () => { it('returns matching users', async () => { const req = makeReq({ body: { byPuid: applicator._id } }); const res = mockRes(); await userCtl.search_post(req, res); expect(res.json).toHaveBeenCalled(); expect(Array.isArray(res._data)).toBe(true); }); }); // ------------------------------------------------------------------------- describe('createUser_post', () => { it('creates a new user and returns it', async () => { const body = { kind: '2', username: `newuser_${Date.now()}@test.com`, password: 'Test@1234', name: 'New Test User', email: `newuser_${Date.now()}@test.com`, active: true, parent: applicator._id, }; const req = makeReq({ body }); const res = mockRes(); await userCtl.createUser_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data._id).toBeDefined(); }); it('throws when body is null', async () => { const req = makeReq({ body: null }); const res = mockRes(); await expect(userCtl.createUser_post(req, res)).rejects.toThrow(); }); }); // ------------------------------------------------------------------------- describe('deleteUser', () => { let tempUser; beforeAll(async () => { tempUser = await Customer.create(mockApplicator()); }); it('deletes a user and returns { ok: true }', async () => { const req = makeReq({ params: { id: String(tempUser._id) } }); const res = mockRes(); await userCtl.deleteUser(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data.ok).toBe(true); }); it('throws when id is invalid', async () => { const req = makeReq({ params: { id: 'not-an-id' } }); const res = mockRes(); await expect(userCtl.deleteUser(req, res)).rejects.toThrow(); }); }); // ------------------------------------------------------------------------- describe('login_post', () => { let loginUser; const loginPassword = 'plainTextPass123'; beforeAll(async () => { // Use a DEVICE user (kind=9) because the APP login path calls Stripe which // is not configured in tests. DEVICE logins skip subscription resolution. const Vehicle = require('../../model/vehicle'); loginUser = await Vehicle.create({ kind: '9', username: `logintest_${Date.now()}@test.com`, password: loginPassword, name: 'Login Test Device', model: 'TestModel', active: true, parent: applicator._id, markedDelete: false, }); }); afterAll(async () => { if (loginUser) { const Vehicle = require('../../model/vehicle'); await Vehicle.deleteMany({ _id: loginUser._id }); } }); it.skip('returns a token on successful login with a DEVICE user (skipped: login_post calls Stripe which is not configured in tests)', async () => { const req = makeReq({ body: { username: loginUser.username, password: loginPassword }, }); const res = mockRes(); await userCtl.login_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data.token).toBeDefined(); }); it('throws when credentials are wrong', async () => { const req = makeReq({ body: { username: loginUser.username, password: 'wrongpassword' }, }); const res = mockRes(); await expect(userCtl.login_post(req, res)).rejects.toThrow(); }); it('throws when username is missing', async () => { const req = makeReq({ body: { password: 'somepass' } }); const res = mockRes(); await expect(userCtl.login_post(req, res)).rejects.toThrow(); }); }); // ------------------------------------------------------------------------- describe('clearTempData_post', () => { it('returns { ok: true }', async () => { const req = makeReq(); const res = mockRes(); await userCtl.clearTempData_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data.ok).toBe(true); }); }); // ------------------------------------------------------------------------- describe('setUserLanguage_post', () => { it('updates user language and returns the updated user', async () => { const req = makeReq({ body: { lang: 'es' } }); const res = mockRes(); await userCtl.setUserLanguage_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data.lang).toBe('es'); }); }); // ------------------------------------------------------------------------- describe('getUserDetail_post', () => { it('returns user details for a valid username', async () => { const req = makeReq({ body: { username: applicator.username } }); const res = mockRes(); await userCtl.getUserDetail_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data).not.toBeNull(); expect(String(res._data._id)).toBe(String(applicator._id)); }); it('returns null when username is missing', async () => { const req = makeReq({ body: {} }); const res = mockRes(); await userCtl.getUserDetail_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data).toBeNull(); }); }); // ------------------------------------------------------------------------- describe('mailPwdReset_post', () => { it('sends a password reset email and returns { result: 1 }', async () => { const mailerMock = require('../../helpers/mailer'); mailerMock.sendResetPasswordEmail.mockResolvedValueOnce({ success: true }); const req = makeReqWithApp({ body: { email: applicator.username } }); const res = mockRes(); await userCtl.mailPwdReset_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data.result).toBe(1); }); it('throws when email is invalid', async () => { const req = makeReq({ body: { email: 'not-an-email' } }); const res = mockRes(); await expect(userCtl.mailPwdReset_post(req, res)).rejects.toThrow(); }); }); // ------------------------------------------------------------------------- describe('validateResetPwdToken_post', () => { it('validates a well-formed reset token', async () => { const secret = `${applicator.password}-${applicator.createdAt.getTime()}`; const token = sign( { email: applicator.username, id: String(applicator._id) }, secret, { expiresIn: '3h' } ); const req = makeReq({ body: { id: String(applicator._id), token } }); const res = mockRes(); await userCtl.validateResetPwdToken_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data.id).toBeDefined(); }); it('throws when params are missing', async () => { const req = makeReq({ body: {} }); const res = mockRes(); await expect(userCtl.validateResetPwdToken_post(req, res)).rejects.toThrow(); }); }); // ------------------------------------------------------------------------- describe('requestEmailVerification_post', () => { it('sends a verification email for a new address and returns { ok: true }', async () => { const mailerMock = require('../../helpers/mailer'); mailerMock.sendEmailVerificationCode.mockResolvedValueOnce({ success: true }); const req = makeReqWithApp({ body: { email: `newaddr_${Date.now()}@test.com`, name: 'New User' }, }); const res = mockRes(); await userCtl.requestEmailVerification_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data.ok).toBe(true); }); it('throws when email is invalid', async () => { const req = makeReq({ body: { email: 'bad-email' } }); const res = mockRes(); await expect(userCtl.requestEmailVerification_post(req, res)).rejects.toThrow(); }); }); // ------------------------------------------------------------------------- describe('verifyEmailCode_post', () => { it('verifies a valid email token and returns a signup token', async () => { const token = sign( { email: `verify_${Date.now()}@test.com` }, process.env.TOKEN_SECRET, { expiresIn: '3h' } ); const req = makeReq({ body: { token } }); const res = mockRes(); await userCtl.verifyEmailCode_post(req, res); expect(res.json).toHaveBeenCalled(); expect(res._data.ok).toBe(true); expect(res._data.token).toBeDefined(); }); it('throws when token is missing', async () => { const req = makeReq({ body: {} }); const res = mockRes(); await expect(userCtl.verifyEmailCode_post(req, res)).rejects.toThrow(); }); }); });