11import { beforeEach , describe , expect , test , vi } from 'vitest' ;
2- import { addMember , removeMember , setMemberRole , setMemberActive } from './membership.service' ;
2+ import { ensureActiveMember , removeMember , setMemberRole , setMemberActive } from './membership.service' ;
33import { prisma , MOCK_USER_WITH_ACCOUNTS } from '@/__mocks__/prisma' ;
44import { OrgRole , type UserToOrg } from '@sourcebot/db' ;
55import { ErrorCode } from '@/lib/errorCodes' ;
@@ -46,14 +46,14 @@ beforeEach(() => {
4646 ( prisma . $transaction as any ) . mockImplementation ( async ( cb : any ) => cb ( prisma ) ) ;
4747} ) ;
4848
49- describe ( 'addMember ' , ( ) => {
49+ describe ( 'ensureActiveMember ' , ( ) => {
5050 test ( 'creates a new active membership when none exists' , async ( ) => {
5151 const created = makeMembership ( ) ;
5252 prisma . user . findUnique . mockResolvedValue ( mockUser ) ;
5353 prisma . userToOrg . findUnique . mockResolvedValue ( null ) ;
5454 prisma . userToOrg . create . mockResolvedValue ( created ) ;
5555
56- const result = await addMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
56+ const result = await ensureActiveMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
5757
5858 expect ( isServiceError ( result ) ) . toBe ( false ) ;
5959 expect ( result ) . toEqual ( created ) ;
@@ -71,7 +71,7 @@ describe('addMember', () => {
7171 prisma . userToOrg . findUnique . mockResolvedValue ( null ) ;
7272 prisma . userToOrg . create . mockResolvedValue ( makeMembership ( { scimExternalId : 'ext-1' } ) ) ;
7373
74- await addMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER , scimExternalId : 'ext-1' } ) ;
74+ await ensureActiveMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER , scimExternalId : 'ext-1' } ) ;
7575
7676 expect ( prisma . userToOrg . create ) . toHaveBeenCalledWith (
7777 expect . objectContaining ( { data : expect . objectContaining ( { scimExternalId : 'ext-1' } ) } ) ,
@@ -83,7 +83,7 @@ describe('addMember', () => {
8383 prisma . userToOrg . findUnique . mockResolvedValue ( null ) ;
8484 prisma . userToOrg . create . mockResolvedValue ( makeMembership ( ) ) ;
8585
86- await addMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
86+ await ensureActiveMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
8787
8888 expect ( prisma . accountRequest . deleteMany ) . toHaveBeenCalledWith ( { where : { requestedById : USER_ID , orgId : ORG_ID } } ) ;
8989 expect ( prisma . invite . deleteMany ) . toHaveBeenCalledWith ( { where : { recipientEmail : mockUser . email , orgId : ORG_ID } } ) ;
@@ -94,33 +94,38 @@ describe('addMember', () => {
9494 prisma . user . findUnique . mockResolvedValue ( mockUser ) ;
9595 prisma . userToOrg . findUnique . mockResolvedValue ( existing ) ;
9696
97- const result = await addMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
97+ const result = await ensureActiveMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
9898
9999 expect ( result ) . toEqual ( existing ) ;
100100 expect ( prisma . userToOrg . create ) . not . toHaveBeenCalled ( ) ;
101101 expect ( mocks . createAudit ) . not . toHaveBeenCalled ( ) ;
102102 } ) ;
103103
104- test ( 'is a no-op when an INACTIVE membership exists (does not reactivate )' , async ( ) => {
104+ test ( 'reactivates an INACTIVE membership (delegates to setMemberActive )' , async ( ) => {
105105 const existing = makeMembership ( { isActive : false } ) ;
106+ const reactivated = makeMembership ( { isActive : true } ) ;
106107 prisma . user . findUnique . mockResolvedValue ( mockUser ) ;
107108 prisma . userToOrg . findUnique . mockResolvedValue ( existing ) ;
109+ prisma . userToOrg . update . mockResolvedValue ( reactivated ) ;
110+ mocks . orgHasAvailability . mockResolvedValue ( true ) ;
108111
109- const result = await addMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
112+ const result = await ensureActiveMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
110113
111- expect ( result ) . toEqual ( existing ) ;
112114 expect ( isServiceError ( result ) ) . toBe ( false ) ;
115+ expect ( result ) . toEqual ( reactivated ) ;
113116 expect ( prisma . userToOrg . create ) . not . toHaveBeenCalled ( ) ;
114- expect ( prisma . userToOrg . update ) . not . toHaveBeenCalled ( ) ;
115- expect ( mocks . createAudit ) . not . toHaveBeenCalled ( ) ;
117+ expect ( prisma . userToOrg . update ) . toHaveBeenCalledWith (
118+ expect . objectContaining ( { data : expect . objectContaining ( { isActive : true } ) } ) ,
119+ ) ;
120+ expect ( mocks . createAudit ) . toHaveBeenCalledWith ( expect . objectContaining ( { action : 'org.member_reactivated' } ) ) ;
116121 } ) ;
117122
118123 test ( 'errors when the org is at seat capacity' , async ( ) => {
119124 prisma . user . findUnique . mockResolvedValue ( mockUser ) ;
120125 prisma . userToOrg . findUnique . mockResolvedValue ( null ) ;
121126 mocks . orgHasAvailability . mockResolvedValue ( false ) ;
122127
123- const result = await addMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
128+ const result = await ensureActiveMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
124129
125130 expect ( isServiceError ( result ) ) . toBe ( true ) ;
126131 expect ( ( result as ServiceError ) . errorCode ) . toBe ( ErrorCode . ORG_SEAT_COUNT_REACHED ) ;
@@ -130,7 +135,7 @@ describe('addMember', () => {
130135 test ( 'errors when the user does not exist' , async ( ) => {
131136 prisma . user . findUnique . mockResolvedValue ( null ) ;
132137
133- const result = await addMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
138+ const result = await ensureActiveMember ( ORG_ID , USER_ID , { actor : ACTOR , role : OrgRole . MEMBER } ) ;
134139
135140 expect ( isServiceError ( result ) ) . toBe ( true ) ;
136141 expect ( prisma . userToOrg . create ) . not . toHaveBeenCalled ( ) ;
@@ -248,11 +253,13 @@ describe('setMemberRole', () => {
248253describe ( 'setMemberActive' , ( ) => {
249254 describe ( 'deactivate' , ( ) => {
250255 test ( 'deactivates an active member and revokes access' , async ( ) => {
256+ const deactivated = makeMembership ( { isActive : false } ) ;
251257 prisma . userToOrg . findUnique . mockResolvedValue ( makeMembership ( { isActive : true } ) ) ;
258+ prisma . userToOrg . update . mockResolvedValue ( deactivated ) ;
252259
253260 const result = await setMemberActive ( ORG_ID , USER_ID , false , { actor : ACTOR } ) ;
254261
255- expect ( result ) . toBeNull ( ) ;
262+ expect ( result ) . toEqual ( deactivated ) ;
256263 expect ( prisma . user . update ) . toHaveBeenCalledWith ( { where : { id : USER_ID } , data : { sessionVersion : { increment : 1 } } } ) ;
257264 expect ( prisma . apiKey . deleteMany ) . toHaveBeenCalledWith ( { where : { createdById : USER_ID , orgId : ORG_ID } } ) ;
258265 expect ( prisma . oAuthToken . deleteMany ) . toHaveBeenCalled ( ) ;
@@ -263,11 +270,12 @@ describe('setMemberActive', () => {
263270 } ) ;
264271
265272 test ( 'is a no-op when already inactive' , async ( ) => {
266- prisma . userToOrg . findUnique . mockResolvedValue ( makeMembership ( { isActive : false } ) ) ;
273+ const existing = makeMembership ( { isActive : false } ) ;
274+ prisma . userToOrg . findUnique . mockResolvedValue ( existing ) ;
267275
268276 const result = await setMemberActive ( ORG_ID , USER_ID , false , { actor : ACTOR } ) ;
269277
270- expect ( result ) . toBeNull ( ) ;
278+ expect ( result ) . toEqual ( existing ) ;
271279 expect ( prisma . userToOrg . update ) . not . toHaveBeenCalled ( ) ;
272280 expect ( prisma . user . update ) . not . toHaveBeenCalled ( ) ;
273281 expect ( mocks . createAudit ) . not . toHaveBeenCalled ( ) ;
@@ -285,12 +293,14 @@ describe('setMemberActive', () => {
285293
286294 describe ( 'reactivate' , ( ) => {
287295 test ( 'reactivates an inactive member when a seat is available' , async ( ) => {
296+ const reactivated = makeMembership ( { isActive : true , scimExternalId : 'ext-1' } ) ;
288297 prisma . userToOrg . findUnique . mockResolvedValue ( makeMembership ( { isActive : false } ) ) ;
298+ prisma . userToOrg . update . mockResolvedValue ( reactivated ) ;
289299 mocks . orgHasAvailability . mockResolvedValue ( true ) ;
290300
291301 const result = await setMemberActive ( ORG_ID , USER_ID , true , { actor : ACTOR , scimExternalId : 'ext-1' } ) ;
292302
293- expect ( result ) . toBeNull ( ) ;
303+ expect ( result ) . toEqual ( reactivated ) ;
294304 expect ( prisma . userToOrg . update ) . toHaveBeenCalledWith (
295305 expect . objectContaining ( { data : expect . objectContaining ( { isActive : true , scimExternalId : 'ext-1' } ) } ) ,
296306 ) ;
@@ -309,22 +319,25 @@ describe('setMemberActive', () => {
309319 } ) ;
310320
311321 test ( 'is a no-op when already active (no audit, no seat check)' , async ( ) => {
312- prisma . userToOrg . findUnique . mockResolvedValue ( makeMembership ( { isActive : true , scimExternalId : 'ext-1' } ) ) ;
322+ const existing = makeMembership ( { isActive : true , scimExternalId : 'ext-1' } ) ;
323+ prisma . userToOrg . findUnique . mockResolvedValue ( existing ) ;
313324
314325 const result = await setMemberActive ( ORG_ID , USER_ID , true , { actor : ACTOR , scimExternalId : 'ext-1' } ) ;
315326
316- expect ( result ) . toBeNull ( ) ;
327+ expect ( result ) . toEqual ( existing ) ;
317328 expect ( prisma . userToOrg . update ) . not . toHaveBeenCalled ( ) ;
318329 expect ( mocks . orgHasAvailability ) . not . toHaveBeenCalled ( ) ;
319330 expect ( mocks . createAudit ) . not . toHaveBeenCalled ( ) ;
320331 } ) ;
321332
322333 test ( 'refreshes externalId when already active and it changed' , async ( ) => {
334+ const refreshed = makeMembership ( { isActive : true , scimExternalId : 'new' } ) ;
323335 prisma . userToOrg . findUnique . mockResolvedValue ( makeMembership ( { isActive : true , scimExternalId : 'old' } ) ) ;
336+ prisma . userToOrg . update . mockResolvedValue ( refreshed ) ;
324337
325338 const result = await setMemberActive ( ORG_ID , USER_ID , true , { actor : ACTOR , scimExternalId : 'new' } ) ;
326339
327- expect ( result ) . toBeNull ( ) ;
340+ expect ( result ) . toEqual ( refreshed ) ;
328341 expect ( prisma . userToOrg . update ) . toHaveBeenCalledWith (
329342 expect . objectContaining ( { data : { scimExternalId : 'new' } } ) ,
330343 ) ;
0 commit comments