Skip to content

Commit a5f6352

Browse files
authored
Merge pull request #3316 from trycompai/main
[comp] Production Deploy
2 parents ab58730 + 67e6730 commit a5f6352

12 files changed

Lines changed: 742 additions & 46 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
const mockUserFindUnique = jest.fn();
2+
const mockMemberFindMany = jest.fn();
3+
const mockMemberCount = jest.fn();
4+
const mockInvitationFindFirst = jest.fn();
5+
6+
jest.mock('@db', () => ({
7+
db: {
8+
user: { findUnique: (...a: unknown[]) => mockUserFindUnique(...a) },
9+
member: {
10+
findMany: (...a: unknown[]) => mockMemberFindMany(...a),
11+
count: (...a: unknown[]) => mockMemberCount(...a),
12+
},
13+
invitation: { findFirst: (...a: unknown[]) => mockInvitationFindFirst(...a) },
14+
},
15+
}));
16+
17+
// Both guards import ./auth.server, which validates SECRET_KEY and pulls in
18+
// better-auth/redis at module load. Stub them so importing the controller is
19+
// hermetic — the test calls the method directly and never runs the guards.
20+
jest.mock('./hybrid-auth.guard', () => ({ HybridAuthGuard: class {} }));
21+
jest.mock('./permission.guard', () => ({
22+
PermissionGuard: class {},
23+
PERMISSIONS_KEY: 'permissions',
24+
}));
25+
26+
import { AuthController } from './auth.controller';
27+
import type { AuthContext } from './types';
28+
29+
const sessionContext = (): AuthContext => ({
30+
organizationId: 'org_1',
31+
authType: 'session',
32+
isApiKey: false,
33+
isPlatformAdmin: false,
34+
userRoles: null,
35+
userId: 'user_1',
36+
userEmail: 'user@example.com',
37+
});
38+
39+
describe('AuthController.getMe — hasInactiveMembership (CS-569)', () => {
40+
const controller = new AuthController();
41+
42+
beforeEach(() => {
43+
jest.clearAllMocks();
44+
mockUserFindUnique.mockResolvedValue({
45+
id: 'user_1',
46+
email: 'user@example.com',
47+
name: 'User',
48+
image: null,
49+
role: 'user',
50+
});
51+
mockInvitationFindFirst.mockResolvedValue(null);
52+
});
53+
54+
it('returns hasInactiveMembership: false for a genuinely new user (no memberships at all)', async () => {
55+
mockMemberFindMany.mockResolvedValue([]);
56+
mockMemberCount.mockResolvedValue(0);
57+
58+
const res = await controller.getMe(sessionContext());
59+
60+
expect(res.organizations).toEqual([]);
61+
expect(res.hasInactiveMembership).toBe(false);
62+
});
63+
64+
it('returns hasInactiveMembership: true for an offboarded user (no active org, only deactivated memberships)', async () => {
65+
mockMemberFindMany.mockResolvedValue([]); // no ACTIVE memberships
66+
mockMemberCount.mockResolvedValue(1); // one deactivated/inactive membership
67+
68+
const res = await controller.getMe(sessionContext());
69+
70+
expect(res.organizations).toEqual([]);
71+
expect(res.hasInactiveMembership).toBe(true);
72+
// The count that distinguishes "offboarded" from "new" must match
73+
// memberships that are deactivated OR no longer active.
74+
expect(mockMemberCount).toHaveBeenCalledWith({
75+
where: {
76+
userId: 'user_1',
77+
OR: [{ deactivated: true }, { isActive: false }],
78+
},
79+
});
80+
});
81+
82+
it('returns hasInactiveMembership: false for an active member', async () => {
83+
mockMemberFindMany.mockResolvedValue([
84+
{
85+
id: 'member_1',
86+
role: 'owner',
87+
organizationId: 'org_1',
88+
organization: {
89+
id: 'org_1',
90+
name: 'Org',
91+
logo: null,
92+
onboardingCompleted: true,
93+
hasAccess: true,
94+
createdAt: new Date(),
95+
},
96+
},
97+
]);
98+
mockMemberCount.mockResolvedValue(0);
99+
100+
const res = await controller.getMe(sessionContext());
101+
102+
expect(res.organizations).toHaveLength(1);
103+
expect(res.hasInactiveMembership).toBe(false);
104+
});
105+
});

‎apps/api/src/auth/auth.controller.ts‎

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -35,44 +35,55 @@ export class AuthController {
3535
return { user: null, organizations: [], pendingInvitation: null };
3636
}
3737

38-
const [user, memberships, pendingInvitation] = await Promise.all([
39-
db.user.findUnique({
40-
where: { id: userId },
41-
select: {
42-
id: true,
43-
email: true,
44-
name: true,
45-
image: true,
46-
role: true,
47-
},
48-
}),
49-
db.member.findMany({
50-
where: { userId, isActive: true, deactivated: false },
51-
select: {
52-
id: true,
53-
role: true,
54-
organizationId: true,
55-
organization: {
56-
select: {
57-
id: true,
58-
name: true,
59-
logo: true,
60-
onboardingCompleted: true,
61-
hasAccess: true,
62-
createdAt: true,
38+
const [user, memberships, pendingInvitation, inactiveMembershipCount] =
39+
await Promise.all([
40+
db.user.findUnique({
41+
where: { id: userId },
42+
select: {
43+
id: true,
44+
email: true,
45+
name: true,
46+
image: true,
47+
role: true,
48+
},
49+
}),
50+
db.member.findMany({
51+
where: { userId, isActive: true, deactivated: false },
52+
select: {
53+
id: true,
54+
role: true,
55+
organizationId: true,
56+
organization: {
57+
select: {
58+
id: true,
59+
name: true,
60+
logo: true,
61+
onboardingCompleted: true,
62+
hasAccess: true,
63+
createdAt: true,
64+
},
6365
},
6466
},
65-
},
66-
orderBy: { createdAt: 'desc' },
67-
}),
68-
db.invitation.findFirst({
69-
where: {
70-
email: authContext.userEmail ?? '',
71-
status: 'pending',
72-
},
73-
select: { id: true },
74-
}),
75-
]);
67+
orderBy: { createdAt: 'desc' },
68+
}),
69+
db.invitation.findFirst({
70+
where: {
71+
email: authContext.userEmail ?? '',
72+
status: 'pending',
73+
},
74+
select: { id: true },
75+
}),
76+
// Count memberships that exist but are no longer active (deactivated
77+
// or removed). Lets the app tell a genuinely new user (no memberships
78+
// at all → onboarding) apart from an offboarded user whose access was
79+
// revoked (→ "access removed" instead of a spurious new org). CS-569.
80+
db.member.count({
81+
where: {
82+
userId,
83+
OR: [{ deactivated: true }, { isActive: false }],
84+
},
85+
}),
86+
]);
7687

7788
return {
7889
user,
@@ -82,6 +93,7 @@ export class AuthController {
8293
memberId: m.id,
8394
})),
8495
pendingInvitation,
96+
hasInactiveMembership: inactiveMembershipCount > 0,
8597
};
8698
}
8799

0 commit comments

Comments
 (0)