sub2api/frontend/src/api/__tests__/auth-oauth-adoption.spec.ts
shaw 9b6dcc57bd feat(affiliate): 完善邀请返利系统
- 修复返利不到账的根因:tryClaimAffiliateRebateAudit 中 PostgreSQL 参数类型推断冲突
  - 补全 OAuth 注册路径(LinuxDo/OIDC/WeChat/Pending Flow)的邀请码绑定
  - 前端 OAuth 注册页面传递 aff_code 参数
  - 新增返利冻结期机制:可配置冻结时间,到期后自动解冻(懒解冻)
  - 新增返利有效期:绑定后 N 天内有效,过期不再产生返利
  - 新增单人返利上限:超出上限部分精确截断
  - 增强返利流程 slog 结构化日志,便于排查问题
  - 已邀请用户列表增加返利明细列
2026-04-26 12:42:35 +08:00

225 lines
6.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const post = vi.fn()
vi.mock('@/api/client', () => ({
apiClient: {
post
}
}))
describe('oauth adoption auth api', () => {
beforeEach(() => {
post.mockReset()
post.mockResolvedValue({ data: {} })
localStorage.clear()
document.cookie = 'oauth_bind_access_token=; Max-Age=0; path=/'
})
it('posts adoption decisions when exchanging pending oauth completion', async () => {
const { exchangePendingOAuthCompletion } = await import('@/api/auth')
await exchangePendingOAuthCompletion({
adoptDisplayName: false,
adoptAvatar: true
})
expect(post).toHaveBeenCalledWith('/auth/oauth/pending/exchange', {
adopt_display_name: false,
adopt_avatar: true
})
})
it('posts bind-login decisions when finalizing pending oauth bind flow', async () => {
const { completePendingOAuthBindLogin } = await import('@/api/auth')
await completePendingOAuthBindLogin({
adoptDisplayName: true,
adoptAvatar: false
})
expect(post).toHaveBeenCalledWith('/auth/oauth/pending/exchange', {
adopt_display_name: true,
adopt_avatar: false
})
})
it('posts linuxdo invitation completion with adoption decisions', async () => {
const { completeLinuxDoOAuthRegistration } = await import('@/api/auth')
await completeLinuxDoOAuthRegistration('invite-code', {
adoptDisplayName: true,
adoptAvatar: false
})
expect(post).toHaveBeenCalledWith('/auth/oauth/linuxdo/complete-registration', {
invitation_code: 'invite-code',
adopt_display_name: true,
adopt_avatar: false
})
})
it('posts linuxdo create-account completion with adoption decisions', async () => {
const { createPendingLinuxDoOAuthAccount } = await import('@/api/auth')
await createPendingLinuxDoOAuthAccount('invite-code', {
adoptDisplayName: false,
adoptAvatar: true
})
expect(post).toHaveBeenCalledWith('/auth/oauth/linuxdo/complete-registration', {
invitation_code: 'invite-code',
adopt_display_name: false,
adopt_avatar: true
})
})
it('posts affiliate code when completing linuxdo oauth registration', async () => {
const { completeLinuxDoOAuthRegistration } = await import('@/api/auth')
await completeLinuxDoOAuthRegistration(
'invite-code',
{
adoptDisplayName: true,
adoptAvatar: false
},
' AFF123 '
)
expect(post).toHaveBeenCalledWith('/auth/oauth/linuxdo/complete-registration', {
invitation_code: 'invite-code',
aff_code: 'AFF123',
adopt_display_name: true,
adopt_avatar: false
})
})
it('posts oidc invitation completion with adoption decisions', async () => {
const { completeOIDCOAuthRegistration } = await import('@/api/auth')
await completeOIDCOAuthRegistration('invite-code', {
adoptDisplayName: false,
adoptAvatar: true
})
expect(post).toHaveBeenCalledWith('/auth/oauth/oidc/complete-registration', {
invitation_code: 'invite-code',
adopt_display_name: false,
adopt_avatar: true
})
})
it('posts oidc create-account completion with adoption decisions', async () => {
const { createPendingOIDCOAuthAccount } = await import('@/api/auth')
await createPendingOIDCOAuthAccount('invite-code', {
adoptDisplayName: true,
adoptAvatar: false
})
expect(post).toHaveBeenCalledWith('/auth/oauth/oidc/complete-registration', {
invitation_code: 'invite-code',
adopt_display_name: true,
adopt_avatar: false
})
})
it('posts wechat invitation completion with adoption decisions', async () => {
const { completeWeChatOAuthRegistration } = await import('@/api/auth')
await completeWeChatOAuthRegistration('invite-code', {
adoptDisplayName: true,
adoptAvatar: true
})
expect(post).toHaveBeenCalledWith('/auth/oauth/wechat/complete-registration', {
invitation_code: 'invite-code',
adopt_display_name: true,
adopt_avatar: true
})
})
it('posts wechat create-account completion with adoption decisions', async () => {
const { createPendingWeChatOAuthAccount } = await import('@/api/auth')
await createPendingWeChatOAuthAccount('invite-code', {
adoptDisplayName: false,
adoptAvatar: false
})
expect(post).toHaveBeenCalledWith('/auth/oauth/wechat/complete-registration', {
invitation_code: 'invite-code',
adopt_display_name: false,
adopt_avatar: false
})
})
it('posts affiliate code when creating pending wechat oauth account', async () => {
const { createPendingWeChatOAuthAccount } = await import('@/api/auth')
await createPendingWeChatOAuthAccount(
'invite-code',
{
adoptDisplayName: false,
adoptAvatar: true
},
'WXAFF'
)
expect(post).toHaveBeenCalledWith('/auth/oauth/wechat/complete-registration', {
invitation_code: 'invite-code',
aff_code: 'WXAFF',
adopt_display_name: false,
adopt_avatar: true
})
})
it('classifies oauth completion results as login or bind', async () => {
const { getOAuthCompletionKind } = await import('@/api/auth')
expect(getOAuthCompletionKind({ access_token: 'access-token' })).toBe('login')
expect(getOAuthCompletionKind({ redirect: '/profile' })).toBe('bind')
})
it('provides bind-login utility helpers for invitation and suggested profile states', async () => {
const {
getPendingOAuthBindLoginKind,
hasPendingOAuthSuggestedProfile,
isPendingOAuthCreateAccountRequired
} = await import('@/api/auth')
expect(getPendingOAuthBindLoginKind({ access_token: 'access-token' })).toBe('login')
expect(getPendingOAuthBindLoginKind({ redirect: '/profile' })).toBe('bind')
expect(
isPendingOAuthCreateAccountRequired({
error: 'invitation_required'
})
).toBe(true)
expect(
isPendingOAuthCreateAccountRequired({
error: 'other'
})
).toBe(false)
expect(
hasPendingOAuthSuggestedProfile({
suggested_display_name: 'OAuth Nick'
})
).toBe(true)
expect(
hasPendingOAuthSuggestedProfile({
suggested_avatar_url: 'https://cdn.example/avatar.png'
})
).toBe(true)
expect(hasPendingOAuthSuggestedProfile({})).toBe(false)
})
it('requests an HttpOnly oauth bind cookie before redirect binding', async () => {
localStorage.setItem('auth_token', 'access-token-value')
const { prepareOAuthBindAccessTokenCookie } = await import('@/api/auth')
await prepareOAuthBindAccessTokenCookie()
expect(post).toHaveBeenCalledWith('/auth/oauth/bind-token')
})
})