|
1 | 1 | /** |
2 | 2 | * @vitest-environment node |
3 | 3 | */ |
4 | | -import { dbChainMockFns, resetDbChainMock } from '@sim/testing' |
| 4 | +import { |
| 5 | + dbChainMock, |
| 6 | + dbChainMockFns, |
| 7 | + queueTableRows, |
| 8 | + resetDbChainMock, |
| 9 | + schemaMock, |
| 10 | +} from '@sim/testing' |
5 | 11 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
6 | 12 |
|
7 | 13 | const { adapter } = vi.hoisted(() => ({ |
@@ -54,6 +60,18 @@ const CONTEXT = { |
54 | 60 | options: [], |
55 | 61 | } |
56 | 62 |
|
| 63 | +const GROUP = { |
| 64 | + status: 'active' as const, |
| 65 | + options: [ |
| 66 | + { |
| 67 | + ...CONTEXT.option, |
| 68 | + authorizationAppId: POLICY.authorizationAppId, |
| 69 | + requiredScopes: POLICY.requiredScopes, |
| 70 | + scopeVersion: POLICY.scopeVersion, |
| 71 | + }, |
| 72 | + ], |
| 73 | +} |
| 74 | + |
57 | 75 | describe('credential group OAuth persistence', () => { |
58 | 76 | beforeEach(() => { |
59 | 77 | vi.clearAllMocks() |
@@ -104,7 +122,9 @@ describe('credential group OAuth persistence', () => { |
104 | 122 | }) |
105 | 123 |
|
106 | 124 | it('preserves completed enrollment state when an account reconnects', async () => { |
107 | | - dbChainMockFns.limit.mockResolvedValueOnce([{ status: 'completed' }]).mockResolvedValueOnce([ |
| 125 | + dbChainMockFns.limit.mockResolvedValueOnce([{ status: 'completed' }]) |
| 126 | + queueTableRows(schemaMock.credentialGroup, [GROUP]) |
| 127 | + queueTableRows(schemaMock.credential, [ |
108 | 128 | { |
109 | 129 | id: 'credential-1', |
110 | 130 | providerSubjectId: 'google-subject-1', |
@@ -142,4 +162,59 @@ describe('credential group OAuth persistence', () => { |
142 | 162 | ) |
143 | 163 | expect(enrollmentUpdate).not.toHaveProperty('completedAt') |
144 | 164 | }) |
| 165 | + |
| 166 | + it('rejects an exchanged grant when the group policy changed before persistence', async () => { |
| 167 | + const nextPolicy = { |
| 168 | + ...POLICY, |
| 169 | + requiredScopes: [...POLICY.requiredScopes, 'https://www.googleapis.com/auth/gmail.readonly'], |
| 170 | + scopeVersion: 2, |
| 171 | + } |
| 172 | + adapter.getPolicy.mockResolvedValueOnce(POLICY).mockResolvedValueOnce(nextPolicy) |
| 173 | + dbChainMockFns.limit.mockResolvedValueOnce([{ status: 'completed' }]) |
| 174 | + queueTableRows(schemaMock.credentialGroup, [ |
| 175 | + { |
| 176 | + ...GROUP, |
| 177 | + options: [ |
| 178 | + { |
| 179 | + ...GROUP.options[0], |
| 180 | + requiredScopes: nextPolicy.requiredScopes, |
| 181 | + scopeVersion: nextPolicy.scopeVersion, |
| 182 | + }, |
| 183 | + ], |
| 184 | + }, |
| 185 | + ]) |
| 186 | + |
| 187 | + await expect( |
| 188 | + completeCredentialGroupOAuth( |
| 189 | + { ...CONTEXT, enrollmentStatus: 'completed' }, |
| 190 | + { |
| 191 | + state: 'state-1', |
| 192 | + provider: 'gmail', |
| 193 | + nonceHash: 'nonce-hash', |
| 194 | + enrollmentId: CONTEXT.enrollmentId, |
| 195 | + credentialGroupId: CONTEXT.credentialGroupId, |
| 196 | + optionId: CONTEXT.option.id, |
| 197 | + authorizationAppId: POLICY.authorizationAppId, |
| 198 | + scopeVersion: POLICY.scopeVersion, |
| 199 | + requiredScopes: POLICY.requiredScopes, |
| 200 | + redirectUri: 'https://sim.ai/api/credential-groups/oauth/gmail/callback', |
| 201 | + codeVerifier: 'verifier', |
| 202 | + invitationToken: 'invitation-token', |
| 203 | + createdAt: Date.now(), |
| 204 | + }, |
| 205 | + 'authorization-code' |
| 206 | + ) |
| 207 | + ).rejects.toThrow('This credential option changed.') |
| 208 | + |
| 209 | + expect(adapter.getPolicy).toHaveBeenLastCalledWith( |
| 210 | + expect.objectContaining({ id: 'option-1' }), |
| 211 | + { |
| 212 | + workspaceId: CONTEXT.workspaceId, |
| 213 | + credentialGroupId: CONTEXT.credentialGroupId, |
| 214 | + executor: dbChainMock.db, |
| 215 | + } |
| 216 | + ) |
| 217 | + expect(dbChainMockFns.update).not.toHaveBeenCalled() |
| 218 | + expect(dbChainMockFns.insert).not.toHaveBeenCalled() |
| 219 | + }) |
145 | 220 | }) |
0 commit comments