Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 557ff54b2b435e5f1e789c6a8a4e1bebf2d7deb6 207 lines 6.1 kB view raw
1import { type Kysely } from 'kysely'; 2 3import { makeTestWithFixture } from '../../test/utils.js'; 4import UserManagementService from './userManagementService.js'; 5import type { UserManagementPg } from './index.js'; 6 7// Mock dependencies 8const mockDb = { 9 selectFrom: jest.fn(), 10 insertInto: jest.fn(), 11 deleteFrom: jest.fn(), 12} as unknown as Kysely<UserManagementPg>; 13 14const mockSendEmail = jest.fn(); 15 16const mockConfigService = { 17 uiUrl: 'http://localhost:3000', 18}; 19 20describe('UserManagementService', () => { 21 const testWithFixtures = makeTestWithFixture(() => { 22 const sut = new UserManagementService( 23 mockDb, 24 mockSendEmail, 25 mockConfigService, 26 ); 27 return { sut }; 28 }); 29 30 beforeEach(() => { 31 jest.clearAllMocks(); 32 }); 33 34 describe('#generatePasswordResetTokenForUser', () => { 35 testWithFixtures( 36 'should generate token and send email for valid user in same org', 37 async ({ sut }) => { 38 const userId = 'user-123'; 39 const orgId = 'org-456'; 40 const email = 'test@example.com'; 41 42 // Mock user lookup 43 const mockSelect = { 44 select: jest.fn().mockReturnThis(), 45 where: jest.fn().mockReturnThis(), 46 executeTakeFirst: jest.fn().mockResolvedValue({ 47 email, 48 orgId, 49 }), 50 }; 51 52 // Mock token insertion 53 const mockInsert = { 54 values: jest.fn().mockReturnThis(), 55 execute: jest.fn().mockResolvedValue([]), 56 }; 57 58 // Mock delete 59 const mockDelete = { 60 where: jest.fn().mockReturnThis(), 61 execute: jest.fn().mockResolvedValue([]), 62 }; 63 64 (mockDb.selectFrom as jest.Mock).mockReturnValue(mockSelect); 65 (mockDb.insertInto as jest.Mock).mockReturnValue(mockInsert); 66 (mockDb.deleteFrom as jest.Mock).mockReturnValue(mockDelete); 67 68 const token = await sut.generatePasswordResetTokenForUser({ 69 userId, 70 invokerOrgId: orgId, 71 }); 72 73 // Verify token was generated (64 char hex string) 74 expect(token).toMatch(/^[a-f0-9]{64}$/); 75 76 // Verify email was sent 77 expect(mockSendEmail).toHaveBeenCalledWith( 78 expect.objectContaining({ 79 to: email, 80 subject: '[Coop] Reset your password', 81 html: expect.stringContaining('password reset'), 82 }), 83 ); 84 85 // Verify token was stored in database 86 expect(mockDb.insertInto).toHaveBeenCalledWith( 87 'user_management_service.password_reset_tokens', 88 ); 89 }, 90 ); 91 92 testWithFixtures( 93 'should throw UnauthorizedError when user is in different org', 94 async ({ sut }) => { 95 const userId = 'user-123'; 96 const userOrgId = 'org-456'; 97 const adminOrgId = 'org-789'; // Different org! 98 99 // Mock user lookup 100 const mockSelect = { 101 select: jest.fn().mockReturnThis(), 102 where: jest.fn().mockReturnThis(), 103 executeTakeFirst: jest.fn().mockResolvedValue({ 104 email: 'test@example.com', 105 orgId: userOrgId, 106 }), 107 }; 108 109 (mockDb.selectFrom as jest.Mock).mockReturnValue(mockSelect); 110 111 await expect( 112 sut.generatePasswordResetTokenForUser({ 113 userId, 114 invokerOrgId: adminOrgId, 115 }), 116 ).rejects.toThrow( 117 expect.objectContaining({ 118 message: expect.stringContaining( 119 'can only reset passwords for users in your organization', 120 ), 121 }), 122 ); 123 124 // Verify email was NOT sent 125 expect(mockSendEmail).not.toHaveBeenCalled(); 126 }, 127 ); 128 129 testWithFixtures( 130 'should throw NotFoundError when user does not exist', 131 async ({ sut }) => { 132 const userId = 'nonexistent-user'; 133 const orgId = 'org-456'; 134 135 // Mock user lookup returning null 136 const mockSelect = { 137 select: jest.fn().mockReturnThis(), 138 where: jest.fn().mockReturnThis(), 139 executeTakeFirst: jest.fn().mockResolvedValue(null), 140 }; 141 142 (mockDb.selectFrom as jest.Mock).mockReturnValue(mockSelect); 143 144 await expect( 145 sut.generatePasswordResetTokenForUser({ 146 userId, 147 invokerOrgId: orgId, 148 }), 149 ).rejects.toThrow( 150 expect.objectContaining({ 151 message: expect.stringContaining('User not found'), 152 }), 153 ); 154 155 // Verify email was NOT sent 156 expect(mockSendEmail).not.toHaveBeenCalled(); 157 }, 158 ); 159 160 testWithFixtures( 161 'should continue if email sending fails (email errors are caught internally)', 162 async ({ sut }) => { 163 const userId = 'user-123'; 164 const orgId = 'org-456'; 165 const email = 'test@example.com'; 166 167 // Mock user lookup 168 const mockSelect = { 169 select: jest.fn().mockReturnThis(), 170 where: jest.fn().mockReturnThis(), 171 executeTakeFirst: jest.fn().mockResolvedValue({ 172 email, 173 orgId, 174 }), 175 }; 176 177 // Mock token insertion 178 const mockInsert = { 179 values: jest.fn().mockReturnThis(), 180 execute: jest.fn().mockResolvedValue([]), 181 }; 182 183 // Mock delete 184 const mockDelete = { 185 where: jest.fn().mockReturnThis(), 186 execute: jest.fn().mockResolvedValue([]), 187 }; 188 189 // Mock email sending to fail (but it's caught internally by sendEmail) 190 mockSendEmail.mockResolvedValue(undefined); // sendEmail catches errors internally 191 192 (mockDb.selectFrom as jest.Mock).mockReturnValue(mockSelect); 193 (mockDb.insertInto as jest.Mock).mockReturnValue(mockInsert); 194 (mockDb.deleteFrom as jest.Mock).mockReturnValue(mockDelete); 195 196 // Should still return token - email service handles its own errors 197 const token = await sut.generatePasswordResetTokenForUser({ 198 userId, 199 invokerOrgId: orgId, 200 }); 201 202 expect(token).toMatch(/^[a-f0-9]{64}$/); 203 expect(mockSendEmail).toHaveBeenCalled(); 204 }, 205 ); 206 }); 207});