@@ -12,6 +12,7 @@ const {
1212 mockCreatePendingInvitation,
1313 mockSendInvitationEmail,
1414 mockCancelPendingInvitation,
15+ mockGrantWorkspaceAccessDirectly,
1516} = vi . hoisted ( ( ) => ( {
1617 mockDbState : {
1718 selectResults : [ ] as any [ ] ,
@@ -22,6 +23,7 @@ const {
2223 mockCreatePendingInvitation : vi . fn ( ) ,
2324 mockSendInvitationEmail : vi . fn ( ) ,
2425 mockCancelPendingInvitation : vi . fn ( ) ,
26+ mockGrantWorkspaceAccessDirectly : vi . fn ( ) ,
2527} ) )
2628
2729function createSelectChain ( ) {
@@ -115,6 +117,10 @@ vi.mock('@/lib/invitations/send', () => ({
115117 cancelPendingInvitation : mockCancelPendingInvitation ,
116118} ) )
117119
120+ vi . mock ( '@/lib/invitations/direct-grant' , ( ) => ( {
121+ grantWorkspaceAccessDirectly : mockGrantWorkspaceAccessDirectly ,
122+ } ) )
123+
118124vi . mock ( '@/lib/messaging/email/validation' , ( ) => ( {
119125 quickValidateEmail : vi . fn ( ( email : string ) => ( { isValid : email . includes ( '@' ) } ) ) ,
120126} ) )
@@ -151,6 +157,7 @@ describe('POST /api/organizations/[id]/invitations', () => {
151157 expiresAt : new Date ( Date . now ( ) + 7 * 24 * 60 * 60 * 1000 ) ,
152158 } )
153159 mockSendInvitationEmail . mockResolvedValue ( { success : true } )
160+ mockGrantWorkspaceAccessDirectly . mockResolvedValue ( { outcome : 'added' , permission : 'write' } )
154161 } )
155162
156163 it ( 'creates a unified invitation and sends a single email' , async ( ) => {
@@ -191,15 +198,15 @@ describe('POST /api/organizations/[id]/invitations', () => {
191198 expect ( mockCancelPendingInvitation ) . not . toHaveBeenCalled ( )
192199 } )
193200
194- it ( 'sends a workspace invitation to an existing member for selected workspaces they lack' , async ( ) => {
201+ it ( 'adds an existing member directly to selected workspaces they lack (no invitation/email) ' , async ( ) => {
195202 mockGetSession . mockResolvedValue (
196203 createSession ( { userId : 'user-1' , email : 'owner@example.com' , name : 'Owner' } )
197204 )
198205 mockDbState . selectResults = [
199206 [ { role : 'owner' } ] ,
200207 [ { name : 'Org One' } ] ,
201- [ { id : 'ws-1' , organizationId : 'org-1' , workspaceMode : 'organization' } ] ,
202- [ { id : 'ws-2' , organizationId : 'org-1' , workspaceMode : 'organization' } ] ,
208+ [ { id : 'ws-1' , name : 'Workspace 1' , organizationId : 'org-1' , workspaceMode : 'organization' } ] ,
209+ [ { id : 'ws-2' , name : 'Workspace 2' , organizationId : 'org-1' , workspaceMode : 'organization' } ] ,
203210 [ { userId : 'user-2' , userEmail : 'member@example.com' } ] ,
204211 [ ] ,
205212 [ { userId : 'user-2' , workspaceId : 'ws-1' } ] ,
@@ -224,27 +231,23 @@ describe('POST /api/organizations/[id]/invitations', () => {
224231 )
225232
226233 expect ( response . status ) . toBe ( 200 )
227- expect ( mockCreatePendingInvitation ) . toHaveBeenCalledTimes ( 1 )
228- expect ( mockCreatePendingInvitation ) . toHaveBeenCalledWith (
234+ expect ( mockCreatePendingInvitation ) . not . toHaveBeenCalled ( )
235+ expect ( mockSendInvitationEmail ) . not . toHaveBeenCalled ( )
236+ expect ( mockGrantWorkspaceAccessDirectly ) . toHaveBeenCalledTimes ( 1 )
237+ expect ( mockGrantWorkspaceAccessDirectly ) . toHaveBeenCalledWith (
229238 expect . objectContaining ( {
230- kind : 'workspace ' ,
239+ userId : 'user-2 ' ,
231240 email : 'member@example.com' ,
241+ workspaceId : 'ws-2' ,
242+ permission : 'write' ,
232243 organizationId : 'org-1' ,
233- membershipIntent : 'internal' ,
234- grants : [ { workspaceId : 'ws-2' , permission : 'write' } ] ,
235- } )
236- )
237- expect ( mockSendInvitationEmail ) . toHaveBeenCalledWith (
238- expect . objectContaining ( {
239- kind : 'workspace' ,
240- email : 'member@example.com' ,
241- grants : [ { workspaceId : 'ws-2' , permission : 'write' } ] ,
242244 } )
243245 )
244246
245247 const body = await response . json ( )
246- expect ( body . data . invitationsSent ) . toBe ( 1 )
247- expect ( body . data . invitedEmails ) . toEqual ( [ 'member@example.com' ] )
248+ expect ( body . data . invitationsSent ) . toBe ( 0 )
249+ expect ( body . data . directlyAdded ) . toEqual ( [ 'member@example.com' ] )
250+ expect ( body . data . directlyAddedCount ) . toBe ( 1 )
248251 expect ( body . data . existingMembers ) . toEqual ( [ ] )
249252 } )
250253
@@ -281,14 +284,14 @@ describe('POST /api/organizations/[id]/invitations', () => {
281284 expect ( mockCreatePendingInvitation ) . not . toHaveBeenCalled ( )
282285 } )
283286
284- it ( 'invites new emails to the organization and existing members to workspaces in one batch' , async ( ) => {
287+ it ( 'invites new emails to the organization and adds existing members to workspaces in one batch' , async ( ) => {
285288 mockGetSession . mockResolvedValue (
286289 createSession ( { userId : 'user-1' , email : 'owner@example.com' , name : 'Owner' } )
287290 )
288291 mockDbState . selectResults = [
289292 [ { role : 'owner' } ] ,
290293 [ { name : 'Org One' } ] ,
291- [ { id : 'ws-1' , organizationId : 'org-1' , workspaceMode : 'organization' } ] ,
294+ [ { id : 'ws-1' , name : 'Workspace 1' , organizationId : 'org-1' , workspaceMode : 'organization' } ] ,
292295 [ { userId : 'user-2' , userEmail : 'member@example.com' } ] ,
293296 [ ] ,
294297 [ ] ,
@@ -310,25 +313,29 @@ describe('POST /api/organizations/[id]/invitations', () => {
310313 )
311314
312315 expect ( response . status ) . toBe ( 200 )
313- expect ( mockCreatePendingInvitation ) . toHaveBeenCalledTimes ( 2 )
316+ expect ( mockCreatePendingInvitation ) . toHaveBeenCalledTimes ( 1 )
314317 expect ( mockCreatePendingInvitation ) . toHaveBeenCalledWith (
315318 expect . objectContaining ( {
316319 kind : 'organization' ,
317320 email : 'new@example.com' ,
318321 grants : [ { workspaceId : 'ws-1' , permission : 'read' } ] ,
319322 } )
320323 )
321- expect ( mockCreatePendingInvitation ) . toHaveBeenCalledWith (
324+ expect ( mockGrantWorkspaceAccessDirectly ) . toHaveBeenCalledTimes ( 1 )
325+ expect ( mockGrantWorkspaceAccessDirectly ) . toHaveBeenCalledWith (
322326 expect . objectContaining ( {
323- kind : 'workspace ' ,
327+ userId : 'user-2 ' ,
324328 email : 'member@example.com' ,
325- grants : [ { workspaceId : 'ws-1' , permission : 'read' } ] ,
329+ workspaceId : 'ws-1' ,
330+ permission : 'read' ,
326331 } )
327332 )
328333
329334 const body = await response . json ( )
330- expect ( body . data . invitationsSent ) . toBe ( 2 )
331- expect ( body . data . invitedEmails ) . toEqual ( [ 'new@example.com' , 'member@example.com' ] )
335+ expect ( body . data . invitationsSent ) . toBe ( 1 )
336+ expect ( body . data . invitedEmails ) . toEqual ( [ 'new@example.com' ] )
337+ expect ( body . data . directlyAdded ) . toEqual ( [ 'member@example.com' ] )
338+ expect ( body . data . directlyAddedCount ) . toBe ( 1 )
332339 } )
333340
334341 it ( 'still rejects existing members on the non-batch organization invite path' , async ( ) => {
0 commit comments