@@ -108,9 +108,9 @@ const mockGetSession = authMockFns.mockGetSession
108108const mockGetWorkspaceWithOwner = permissionsMockFns . mockGetWorkspaceWithOwner
109109
110110import { UPGRADE_TO_INVITE_REASON } from '@/lib/workspaces/policy-constants'
111- import { POST } from '@/app/api/workspaces/invitations/route'
111+ import { POST } from '@/app/api/workspaces/invitations/batch/ route'
112112
113- describe ( 'POST /api/workspaces/invitations' , ( ) => {
113+ describe ( 'POST /api/workspaces/invitations/batch ' , ( ) => {
114114 beforeEach ( ( ) => {
115115 vi . clearAllMocks ( )
116116 mockDbResults . value = [ ]
@@ -169,8 +169,7 @@ describe('POST /api/workspaces/invitations', () => {
169169
170170 const request = createMockRequest ( 'POST' , {
171171 workspaceId : 'workspace-1' ,
172- email : 'new@example.com' ,
173- permission : 'read' ,
172+ invitations : [ { email : 'new@example.com' , permission : 'read' } ] ,
174173 } )
175174
176175 const response = await POST ( request )
@@ -201,8 +200,7 @@ describe('POST /api/workspaces/invitations', () => {
201200
202201 const request = createMockRequest ( 'POST' , {
203202 workspaceId : 'workspace-1' ,
204- email : 'new@example.com' ,
205- permission : 'read' ,
203+ invitations : [ { email : 'new@example.com' , permission : 'read' } ] ,
206204 } )
207205
208206 const response = await POST ( request )
@@ -213,7 +211,7 @@ describe('POST /api/workspaces/invitations', () => {
213211 expect ( mockCreatePendingInvitation ) . not . toHaveBeenCalled ( )
214212 } )
215213
216- it ( 'rejects org-owned invites when the organization has no available seats' , async ( ) => {
214+ it ( 'reports org-owned invites as failed when the organization has no available seats' , async ( ) => {
217215 mockGetWorkspaceWithOwner . mockResolvedValueOnce ( {
218216 id : 'workspace-1' ,
219217 name : 'Org Workspace' ,
@@ -240,15 +238,20 @@ describe('POST /api/workspaces/invitations', () => {
240238
241239 const request = createMockRequest ( 'POST' , {
242240 workspaceId : 'workspace-1' ,
243- email : 'new@example.com' ,
244- permission : 'read' ,
241+ invitations : [ { email : 'new@example.com' , permission : 'read' } ] ,
245242 } )
246243
247244 const response = await POST ( request )
248245 const data = await response . json ( )
249246
250- expect ( response . status ) . toBe ( 400 )
251- expect ( data . error ) . toContain ( 'No available seats' )
247+ expect ( response . status ) . toBe ( 200 )
248+ expect ( data . success ) . toBe ( false )
249+ expect ( data . failed ) . toEqual ( [
250+ {
251+ email : 'new@example.com' ,
252+ error : 'No available seats. Currently using 5 of 5 seats.' ,
253+ } ,
254+ ] )
252255 expect ( mockValidateSeatAvailability ) . toHaveBeenCalledWith ( 'org-1' , 1 )
253256 expect ( mockCreatePendingInvitation ) . not . toHaveBeenCalled ( )
254257 } )
@@ -281,16 +284,15 @@ describe('POST /api/workspaces/invitations', () => {
281284
282285 const request = createMockRequest ( 'POST' , {
283286 workspaceId : 'workspace-1' ,
284- email : 'new@example.com' ,
285- permission : 'read' ,
287+ invitations : [ { email : 'new@example.com' , permission : 'read' } ] ,
286288 } )
287289
288290 const response = await POST ( request )
289291 const data = await response . json ( )
290292
291293 expect ( response . status ) . toBe ( 200 )
292294 expect ( data . success ) . toBe ( true )
293- expect ( data . invitation . membershipIntent ) . toBe ( 'external' )
295+ expect ( data . invitations [ 0 ] . membershipIntent ) . toBe ( 'external' )
294296 expect ( mockValidateSeatAvailability ) . not . toHaveBeenCalled ( )
295297 expect ( mockCreatePendingInvitation ) . toHaveBeenCalledWith (
296298 expect . objectContaining ( {
@@ -316,8 +318,7 @@ describe('POST /api/workspaces/invitations', () => {
316318
317319 const request = createMockRequest ( 'POST' , {
318320 workspaceId : 'workspace-1' ,
319- email : 'new@example.com' ,
320- permission : 'write' ,
321+ invitations : [ { email : 'new@example.com' , permission : 'write' } ] ,
321322 } )
322323
323324 const response = await POST ( request )
@@ -337,6 +338,40 @@ describe('POST /api/workspaces/invitations', () => {
337338 expect ( mockValidateSeatAvailability ) . not . toHaveBeenCalled ( )
338339 } )
339340
341+ it ( 'creates multiple workspace invitations in one batch request' , async ( ) => {
342+ mockDbResults . value = [ [ { permissionType : 'admin' } ] , [ ] , [ ] ]
343+ mockCreatePendingInvitation
344+ . mockResolvedValueOnce ( {
345+ invitationId : 'inv-1' ,
346+ token : 'tok-1' ,
347+ expiresAt : new Date ( Date . now ( ) + 7 * 24 * 60 * 60 * 1000 ) ,
348+ } )
349+ . mockResolvedValueOnce ( {
350+ invitationId : 'inv-2' ,
351+ token : 'tok-2' ,
352+ expiresAt : new Date ( Date . now ( ) + 7 * 24 * 60 * 60 * 1000 ) ,
353+ } )
354+
355+ const request = createMockRequest ( 'POST' , {
356+ workspaceId : 'workspace-1' ,
357+ invitations : [
358+ { email : 'first@example.com' , permission : 'read' } ,
359+ { email : 'second@example.com' , permission : 'write' } ,
360+ ] ,
361+ } )
362+
363+ const response = await POST ( request )
364+ const data = await response . json ( )
365+
366+ expect ( response . status ) . toBe ( 200 )
367+ expect ( data . success ) . toBe ( true )
368+ expect ( data . successful ) . toEqual ( [ 'first@example.com' , 'second@example.com' ] )
369+ expect ( data . failed ) . toEqual ( [ ] )
370+ expect ( data . invitations ) . toHaveLength ( 2 )
371+ expect ( mockCreatePendingInvitation ) . toHaveBeenCalledTimes ( 2 )
372+ expect ( mockSendInvitationEmail ) . toHaveBeenCalledTimes ( 2 )
373+ } )
374+
340375 it ( 'rolls back the unified invitation when email delivery fails' , async ( ) => {
341376 mockGetWorkspaceWithOwner . mockResolvedValueOnce ( {
342377 id : 'workspace-1' ,
@@ -354,13 +389,18 @@ describe('POST /api/workspaces/invitations', () => {
354389
355390 const request = createMockRequest ( 'POST' , {
356391 workspaceId : 'workspace-1' ,
357- email : 'new@example.com' ,
358- permission : 'read' ,
392+ invitations : [ { email : 'new@example.com' , permission : 'read' } ] ,
359393 } )
360394
361395 const response = await POST ( request )
362396
363- expect ( response . status ) . toBe ( 502 )
397+ expect ( response . status ) . toBe ( 200 )
398+ await expect ( response . json ( ) ) . resolves . toEqual (
399+ expect . objectContaining ( {
400+ success : false ,
401+ failed : [ { email : 'new@example.com' , error : 'mailer unavailable' } ] ,
402+ } )
403+ )
364404 expect ( mockCancelPendingInvitation ) . toHaveBeenCalledWith ( 'inv-1' )
365405 } )
366406} )
0 commit comments