@@ -32,7 +32,11 @@ const teamPlan: PlanFeatures = {
3232} ;
3333
3434const createContext = (
35- overrides ?: Partial < { planFeatures : PlanFeatures ; userId : string } >
35+ overrides ?: Partial < {
36+ planFeatures : PlanFeatures ;
37+ ownerPlanFeatures : PlanFeatures ;
38+ userId : string ;
39+ } >
3640) : AppContext =>
3741 ( {
3842 ...testContext ,
@@ -43,7 +47,8 @@ const createContext = (
4347 planFeatures : overrides ?. planFeatures ?? teamPlan ,
4448 purchases : [ ] ,
4549 trpcCache : { setMaxAge : ( ) => { } } ,
46- getOwnerPlanFeatures : async ( ) => overrides ?. planFeatures ?? teamPlan ,
50+ getOwnerPlanFeatures : async ( ) =>
51+ overrides ?. ownerPlanFeatures ?? overrides ?. planFeatures ?? teamPlan ,
4752 } ) as unknown as AppContext ;
4853
4954// ---------------------------------------------------------------------------
@@ -134,7 +139,11 @@ const setupAddMemberMocks = (opts?: {
134139
135140describe ( "addMember" , ( ) => {
136141 test ( "rejects when maxWorkspaces <= 1 (free/pro plan)" , async ( ) => {
137- // No DB mocks needed — the check happens before any queries.
142+ server . use (
143+ db . get ( "Workspace" , ( ) =>
144+ json ( { id : "ws-1" , userId : "owner-1" , isDeleted : false } )
145+ )
146+ ) ;
138147 const ctx = createContext ( { planFeatures : defaultPlanFeatures } ) ;
139148 const caller = createCaller ( ctx ) ;
140149
@@ -150,6 +159,35 @@ describe("addMember", () => {
150159 }
151160 } ) ;
152161
162+ test ( "rejects non-owners before billing sync" , async ( ) => {
163+ let paymentWorkerCalled = false ;
164+
165+ server . use (
166+ db . get ( "Workspace" , ( ) =>
167+ json ( { id : "ws-1" , userId : "owner-1" , isDeleted : false } )
168+ ) ,
169+ http . post ( `${ env . PAYMENT_WORKER_URL } /seats/sync` , ( ) => {
170+ paymentWorkerCalled = true ;
171+ return HttpResponse . json ( { type : "success" , seats : 1 } ) ;
172+ } )
173+ ) ;
174+
175+ const ctx = createContext ( { userId : "member-1" } ) ;
176+ const caller = createCaller ( ctx ) ;
177+
178+ const result = await caller . addMember ( {
179+ workspaceId : "ws-1" ,
180+ email : "invitee@test.com" ,
181+ relation : "editors" ,
182+ } ) ;
183+
184+ expect ( result . success ) . toBe ( false ) ;
185+ if ( ! result . success ) {
186+ expect ( result . error ) . toContain ( "Only the workspace owner" ) ;
187+ }
188+ expect ( paymentWorkerCalled ) . toBe ( false ) ;
189+ } ) ;
190+
153191 test ( "rejects when workspace seat limit reached" , async ( ) => {
154192 const plan : PlanFeatures = { ...teamPlan , maxSeatsPerWorkspace : 2 } ;
155193 const ctx = createContext ( { planFeatures : plan } ) ;
@@ -320,6 +358,11 @@ describe("removeMember", () => {
320358
321359describe ( "syncSeats" , ( ) => {
322360 test ( "rejects when maxWorkspaces <= 1" , async ( ) => {
361+ server . use (
362+ db . get ( "Workspace" , ( ) =>
363+ json ( { id : "ws-1" , userId : "owner-1" , isDeleted : false } )
364+ )
365+ ) ;
323366 const ctx = createContext ( { planFeatures : defaultPlanFeatures } ) ;
324367 const caller = createCaller ( ctx ) ;
325368
@@ -335,6 +378,9 @@ describe("syncSeats", () => {
335378 let capturedBody : { workspaceId : string ; delta ?: number } | undefined ;
336379
337380 server . use (
381+ db . get ( "Workspace" , ( ) =>
382+ json ( { id : "ws-1" , userId : "owner-1" , isDeleted : false } )
383+ ) ,
338384 http . post ( `${ env . PAYMENT_WORKER_URL } /seats/sync` , async ( { request } ) => {
339385 capturedBody = ( await request . json ( ) ) as typeof capturedBody ;
340386 return HttpResponse . json ( { type : "success" , seats : 3 } ) ;
@@ -376,7 +422,13 @@ describe("listMembers", () => {
376422 json ( { id : "ws-1" , userId : "owner-1" , isDeleted : false } )
377423 ) ,
378424 // listMembers: WorkspaceMember SELECT (members list + access check)
379- db . get ( "WorkspaceMember" , ( ) => json ( members ) ) ,
425+ db . get ( "WorkspaceMember" , ( { request } ) => {
426+ const url = new URL ( request . url ) ;
427+ if ( url . searchParams . has ( "userId" ) ) {
428+ return json ( { userId : url . searchParams . get ( "userId" ) } ) ;
429+ }
430+ return json ( members ) ;
431+ } ) ,
380432 // listMembers: Notification GET (pending invites for this workspace)
381433 db . get ( "Notification" , ( ) => json ( [ ] ) ) ,
382434 // listMembers: User batch lookup
@@ -449,6 +501,23 @@ describe("listMembers", () => {
449501 }
450502 } ) ;
451503
504+ test ( "maxSeats uses workspace owner's seats for member callers" , async ( ) => {
505+ setupListMembersMocks ( { transactionLog : null } ) ;
506+
507+ const ctx = createContext ( {
508+ userId : "m-1" ,
509+ planFeatures : defaultPlanFeatures ,
510+ ownerPlanFeatures : teamPlan ,
511+ } ) ;
512+ const caller = createCaller ( ctx ) ;
513+ const result = await caller . listMembers ( { workspaceId : "ws-1" } ) ;
514+
515+ expect ( result . success ) . toBe ( true ) ;
516+ if ( result . success ) {
517+ expect ( result . data . maxSeats ) . toBe ( 4 ) ;
518+ }
519+ } ) ;
520+
452521 test ( "returns owner, members, and pendingInvites" , async ( ) => {
453522 setupListMembersMocks ( { memberCount : 2 , transactionLog : null } ) ;
454523
0 commit comments