feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
131
apps/mobile-rn/__tests__/identity-linking.test.ts
Normal file
131
apps/mobile-rn/__tests__/identity-linking.test.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import {
|
||||
IdentityManagementService,
|
||||
MANAGED_OAUTH_PROVIDERS,
|
||||
} from '../src/features/auth/identity-linking'
|
||||
|
||||
jest.mock('../src/lib/supabase', () => ({ supabase: { auth: {} } }))
|
||||
jest.mock('../src/lib/auth-redirect', () => ({ AUTH_REDIRECT_URL: 'd3ro-voice://auth-callback' }))
|
||||
|
||||
const USER_ID = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
function identity(provider: string, identityId = `${provider}-identity`) {
|
||||
return {
|
||||
id: USER_ID,
|
||||
user_id: USER_ID,
|
||||
identity_id: identityId,
|
||||
provider,
|
||||
}
|
||||
}
|
||||
|
||||
function createAuth(overrides: Record<string, unknown> = {}) {
|
||||
const getUser = jest.fn(async () => ({
|
||||
data: { user: { id: USER_ID, app_metadata: { provider: 'email' } } },
|
||||
error: null,
|
||||
}))
|
||||
const getUserIdentities = jest.fn(async () => ({
|
||||
data: { identities: [identity('email'), identity('google')] },
|
||||
error: null,
|
||||
}))
|
||||
const linkIdentity = jest.fn(async () => ({
|
||||
data: { provider: 'github', url: 'https://provider.example/authorize' },
|
||||
error: null,
|
||||
}))
|
||||
const unlinkIdentity = jest.fn(async () => ({ data: {}, error: null }))
|
||||
return {
|
||||
getUser,
|
||||
getUserIdentities,
|
||||
linkIdentity,
|
||||
unlinkIdentity,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('manual identity linking safety boundary', () => {
|
||||
it('only exposes the three mobile capability providers', () => {
|
||||
expect(MANAGED_OAUTH_PROVIDERS).toEqual(['google', 'github', 'apple'])
|
||||
})
|
||||
|
||||
it('uses the canonical PKCE callback and blocks duplicate provider linking', async () => {
|
||||
const auth = createAuth()
|
||||
const service = new IdentityManagementService(auth as never)
|
||||
|
||||
await expect(service.createLinkUrl('github', USER_ID)).resolves.toEqual({
|
||||
ok: true,
|
||||
value: { url: 'https://provider.example/authorize' },
|
||||
})
|
||||
expect(auth.linkIdentity).toHaveBeenCalledWith({
|
||||
provider: 'github',
|
||||
options: { redirectTo: 'd3ro-voice://auth-callback', skipBrowserRedirect: true },
|
||||
})
|
||||
|
||||
await expect(service.createLinkUrl('google', USER_ID)).resolves.toEqual({
|
||||
ok: false,
|
||||
code: 'already_linked',
|
||||
})
|
||||
expect(auth.linkIdentity).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('fails closed if the account changes during the provider URL request', async () => {
|
||||
const getUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ data: { user: { id: USER_ID, app_metadata: { provider: 'email' } } }, error: null })
|
||||
.mockResolvedValueOnce({ data: { user: { id: USER_ID, app_metadata: { provider: 'email' } } }, error: null })
|
||||
.mockResolvedValueOnce({ data: { user: { id: 'other-account', app_metadata: { provider: 'email' } } }, error: null })
|
||||
const auth = createAuth({ getUser })
|
||||
const service = new IdentityManagementService(auth as never)
|
||||
|
||||
await expect(service.createLinkUrl('github', USER_ID)).resolves.toEqual({
|
||||
ok: false,
|
||||
code: 'account_changed',
|
||||
})
|
||||
})
|
||||
|
||||
it('never unlinks email, the last identity, or the provider used for the current login', async () => {
|
||||
const auth = createAuth()
|
||||
const service = new IdentityManagementService(auth as never)
|
||||
|
||||
await expect(service.unlink('email-identity', USER_ID)).resolves.toEqual({
|
||||
ok: false,
|
||||
code: 'email_identity_protected',
|
||||
})
|
||||
expect(auth.unlinkIdentity).not.toHaveBeenCalled()
|
||||
|
||||
auth.getUserIdentities.mockResolvedValueOnce({
|
||||
data: { identities: [identity('google')] },
|
||||
error: null,
|
||||
})
|
||||
await expect(service.unlink('google-identity', USER_ID)).resolves.toEqual({
|
||||
ok: false,
|
||||
code: 'last_identity_protected',
|
||||
})
|
||||
|
||||
auth.getUser.mockImplementation(async () => ({
|
||||
data: { user: { id: USER_ID, app_metadata: { provider: 'google' } } },
|
||||
error: null,
|
||||
}))
|
||||
auth.getUserIdentities.mockResolvedValueOnce({
|
||||
data: { identities: [identity('email'), identity('google')] },
|
||||
error: null,
|
||||
})
|
||||
await expect(service.unlink('google-identity', USER_ID)).resolves.toEqual({
|
||||
ok: false,
|
||||
code: 'last_login_protected',
|
||||
})
|
||||
expect(auth.unlinkIdentity).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('maps session failures to a sanitized reauthentication requirement', async () => {
|
||||
const auth = createAuth({
|
||||
getUser: jest.fn(async () => ({
|
||||
data: { user: null },
|
||||
error: Object.assign(new Error('JWT expired with provider internals'), { code: 'jwt_expired' }),
|
||||
})),
|
||||
})
|
||||
const service = new IdentityManagementService(auth as never)
|
||||
|
||||
await expect(service.list(USER_ID)).resolves.toEqual({
|
||||
ok: false,
|
||||
code: 'reauthentication_required',
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue