chore: 导入清理前的完整工程状态(含 Capacitor 原生壳,作为回滚备份点)
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import type { AcquisitionContext, SsoAuthorizationResult } from '@yunlefun/sso'
|
||||
import type { YunlefunSsoConfig } from '../apps/web/app/utils/yunlefunSsoConfig'
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import { SsoIdentityAdoptionError } from '@yunlefun/sso/browser'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createCloudbaseIdentityAdapter,
|
||||
createNativeYunlefunAuthorizationAdapter,
|
||||
createWebYunlefunAuthorizationAdapter,
|
||||
} from '../apps/web/app/adapters/yunlefunAuth'
|
||||
|
||||
vi.mock('@capacitor/browser', () => ({
|
||||
Browser: {
|
||||
close: vi.fn(),
|
||||
open: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@capacitor/preferences', () => ({
|
||||
Preferences: {
|
||||
get: vi.fn(async () => ({ value: null })),
|
||||
remove: vi.fn(),
|
||||
set: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const config: YunlefunSsoConfig = {
|
||||
cloudbaseEnv: 'yunlefun-production',
|
||||
exchange: {
|
||||
exchangeUrl: 'https://api.yunle.fun/sso-ticket',
|
||||
},
|
||||
redirect: {
|
||||
clientId: 'cook-web',
|
||||
redirectUri: 'https://cook.yunyoujun.cn/auth/callback',
|
||||
scope: ['identity:bootstrap'],
|
||||
ssoOrigin: 'https://www.yunle.fun',
|
||||
},
|
||||
}
|
||||
|
||||
const hostAuthorization: SsoAuthorizationResult = {
|
||||
ok: true,
|
||||
clientId: 'cook-web',
|
||||
code: 'c'.repeat(43),
|
||||
codeVerifier: 'v'.repeat(43),
|
||||
issuer: 'https://www.yunle.fun',
|
||||
nonce: 'n'.repeat(43),
|
||||
redirectUri: 'https://cook.yunyoujun.cn/auth/callback',
|
||||
scope: ['identity:bootstrap'],
|
||||
}
|
||||
|
||||
const touch = {
|
||||
capturedAt: 1000,
|
||||
landingHost: 'cook.yunle.fun',
|
||||
landingPath: '/random',
|
||||
referrerHost: 'www.baidu.com',
|
||||
utmSource: 'baidu',
|
||||
utmMedium: 'organic',
|
||||
utmCampaign: '',
|
||||
}
|
||||
const acquisition: AcquisitionContext = { version: 1, firstTouch: touch, registerTouch: touch }
|
||||
|
||||
describe('web authorization adapter', () => {
|
||||
it('passes the same captured context to host authorization and redirect fallback', async () => {
|
||||
const requestHostAuthorization = vi.fn(async () => null)
|
||||
const startRedirect = vi.fn()
|
||||
const adapter = createWebYunlefunAuthorizationAdapter(config, {
|
||||
readAcquisition: () => acquisition,
|
||||
requestHostAuthorization,
|
||||
startRedirect,
|
||||
})
|
||||
await adapter.start()
|
||||
expect(requestHostAuthorization).toHaveBeenCalledWith({ ...config.redirect, prompt: 'consent', acquisition })
|
||||
expect(startRedirect).toHaveBeenCalledWith({ ...config.redirect, acquisition })
|
||||
})
|
||||
|
||||
it('uses the YunLeFun host consent sheet without starting a redirect', async () => {
|
||||
const requestHostAuthorization = vi.fn(async () => hostAuthorization)
|
||||
const startRedirect = vi.fn()
|
||||
const adapter = createWebYunlefunAuthorizationAdapter(config, {
|
||||
requestHostAuthorization,
|
||||
startRedirect,
|
||||
})
|
||||
|
||||
await expect(adapter.start()).resolves.toEqual(hostAuthorization)
|
||||
expect(requestHostAuthorization).toHaveBeenCalledWith({
|
||||
...config.redirect,
|
||||
prompt: 'consent',
|
||||
})
|
||||
expect(startRedirect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to the top-level web redirect outside the YunLeFun host', async () => {
|
||||
const startRedirect = vi.fn()
|
||||
const adapter = createWebYunlefunAuthorizationAdapter(config, {
|
||||
requestHostAuthorization: async () => null,
|
||||
startRedirect,
|
||||
})
|
||||
|
||||
await expect(adapter.start()).resolves.toBeNull()
|
||||
expect(startRedirect).toHaveBeenCalledWith(config.redirect)
|
||||
})
|
||||
|
||||
it('does not bypass an explicit host denial with a web redirect', async () => {
|
||||
const startRedirect = vi.fn()
|
||||
const adapter = createWebYunlefunAuthorizationAdapter(config, {
|
||||
requestHostAuthorization: async () => {
|
||||
throw new SsoIdentityAdoptionError(
|
||||
'Host authorization was denied',
|
||||
'access_denied',
|
||||
)
|
||||
},
|
||||
startRedirect,
|
||||
})
|
||||
|
||||
await expect(adapter.start()).resolves.toEqual({
|
||||
ok: false,
|
||||
reason: 'access_denied',
|
||||
})
|
||||
expect(startRedirect).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cloudbase identity adapter', () => {
|
||||
it('rejects a sign-out response that contains a CloudBase error', async () => {
|
||||
const identity = createCloudbaseIdentityAdapter(config, {
|
||||
getAuth: async () => ({
|
||||
getSession: async () => ({ data: { session: null } }),
|
||||
getUser: async () => ({ data: { user: null } }),
|
||||
signInWithCustomTicket: async () => ({}),
|
||||
signOut: async () => ({ error: new Error('remote sign-out failed') }),
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(identity.signOut()).rejects.toThrow('CloudBase sign-out failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('native authorization adapter', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('clears the pending transaction when authorization is cancelled', async () => {
|
||||
const nativeConfig: YunlefunSsoConfig = {
|
||||
...config,
|
||||
redirect: {
|
||||
...config.redirect,
|
||||
clientId: 'cook-mobile',
|
||||
redirectUri: 'https://cook.yunyoujun.cn/auth/callback?platform=native',
|
||||
},
|
||||
}
|
||||
const adapter = createNativeYunlefunAuthorizationAdapter(nativeConfig, async () => null)
|
||||
|
||||
await adapter.start()
|
||||
expect(Preferences.set).toHaveBeenCalledOnce()
|
||||
expect(Browser.open).toHaveBeenCalledOnce()
|
||||
|
||||
await adapter.cancel()
|
||||
expect(Preferences.remove).toHaveBeenCalledWith({
|
||||
key: 'cook:sso:v3:native-transaction',
|
||||
})
|
||||
})
|
||||
|
||||
it('serializes acquisition into the actual native browser SSO URL', async () => {
|
||||
const adapter = createNativeYunlefunAuthorizationAdapter(config, async () => null, () => acquisition)
|
||||
await adapter.start()
|
||||
const url = new URL(vi.mocked(Browser.open).mock.calls[0]![0].url)
|
||||
expect(JSON.parse(url.searchParams.get('acquisition')!)).toEqual(acquisition)
|
||||
expect(url.searchParams.get('client_id')).toBe('cook-web')
|
||||
expect(url.searchParams.get('code_challenge_method')).toBe('S256')
|
||||
const transaction = JSON.parse(vi.mocked(Preferences.set).mock.calls[0]![0].value)
|
||||
expect(transaction.acquisition).toEqual(acquisition)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user