Files
easyai-ai-gateway/apps/web/src/lib/oidc.test.ts
T
chengcheng d956524690
ci / verify (pull_request) Successful in 22m7s
fix(web): 修复相对 OIDC 登录地址跳转
2026-07-20 15:26:44 +08:00

125 lines
5.0 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
describe('OIDC BFF navigation', () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
vi.resetModules();
});
it('sends only returnTo to the Gateway login endpoint', async () => {
vi.stubEnv('VITE_GATEWAY_API_BASE_URL', 'https://gateway.example.com/gateway-api');
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ enabled: true, oidcLogin: true, loginUrl: '/api/v1/auth/oidc/login', logoutUrl: '/api/v1/auth/oidc/logout', status: 'active' }),
}));
const assign = vi.fn();
vi.stubGlobal('window', {
location: { pathname: '/workspace', search: '?tab=wallet', hash: '#balance', assign },
});
const { startOIDCLogin } = await import('./oidc');
await startOIDCLogin();
const target = new URL(String(assign.mock.calls[0]?.[0]));
expect(target.pathname).toBe('/gateway-api/api/v1/auth/oidc/login');
expect(target.searchParams.get('returnTo')).toBe('/workspace?tab=wallet#balance');
expect(target.searchParams.has('client_id')).toBe(false);
expect(target.searchParams.has('code_challenge')).toBe(false);
});
it('resolves the production relative API base against the browser origin', async () => {
vi.stubEnv('VITE_GATEWAY_API_BASE_URL', '/gateway-api');
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
enabled: true,
oidcLogin: true,
loginUrl: '/api/v1/auth/oidc/login',
logoutUrl: '/api/v1/auth/oidc/logout',
status: 'active',
}),
}));
const assign = vi.fn();
vi.stubGlobal('window', {
location: { origin: 'https://ai.51easyai.com', pathname: '/workspace/overview', search: '', hash: '', assign },
});
const { startOIDCLogin } = await import('./oidc');
await startOIDCLogin();
const target = new URL(String(assign.mock.calls[0]?.[0]));
expect(target.origin).toBe('https://ai.51easyai.com');
expect(target.pathname).toBe('/gateway-api/api/v1/auth/oidc/login');
expect(target.searchParams.get('returnTo')).toBe('/workspace/overview');
});
it('submits logout as a top-level POST without exposing tokens', async () => {
vi.stubEnv('VITE_GATEWAY_API_BASE_URL', 'https://gateway.example.com/gateway-api');
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ enabled: true, oidcLogin: true, loginUrl: '/api/v1/auth/oidc/login', logoutUrl: '/api/v1/auth/oidc/logout', status: 'active' }),
}));
const submit = vi.fn();
const form = { method: '', action: '', style: { display: '' }, submit };
const appendChild = vi.fn();
vi.stubGlobal('document', { createElement: vi.fn(() => form), body: { appendChild } });
const { startOIDCLogout } = await import('./oidc');
await expect(startOIDCLogout()).resolves.toBe(true);
expect(form.method).toBe('POST');
expect(form.action).toBe('https://gateway.example.com/gateway-api/api/v1/auth/oidc/logout');
expect(Object.keys(form)).not.toContain('accessToken');
expect(submit).toHaveBeenCalledOnce();
});
it('turns an invalid login transaction callback into an explicit retry action', async () => {
const replaceState = vi.fn();
vi.stubGlobal('window', {
location: { pathname: '/', search: '?oidcError=OIDC_LOGIN_INVALID', hash: '#top' },
history: { replaceState },
});
const { consumeOIDCCallbackError } = await import('./oidc');
expect(consumeOIDCCallbackError()).toEqual({
code: 'OIDC_LOGIN_INVALID',
message: '统一认证登录事务无效或已过期,请重新登录',
retryable: true,
});
expect(replaceState).toHaveBeenCalledWith({}, '', '/#top');
});
it('explains a missing transaction cookie without retaining diagnostic query parameters', async () => {
const replaceState = vi.fn();
vi.stubGlobal('window', {
location: {
pathname: '/',
search: '?tab=login&oidcError=OIDC_LOGIN_INVALID&oidcErrorReason=COOKIE_MISSING&oidcDiagnosticId=diag-123',
hash: '',
},
history: { replaceState },
});
const { consumeOIDCCallbackError } = await import('./oidc');
expect(consumeOIDCCallbackError()).toEqual({
code: 'OIDC_LOGIN_INVALID',
reason: 'COOKIE_MISSING',
diagnosticId: 'diag-123',
message: '浏览器未携带统一认证登录事务 Cookie,请允许 localhost Cookie 后重新登录',
retryable: true,
});
expect(replaceState).toHaveBeenCalledWith({}, '', '/?tab=login');
});
it('does not offer transaction retry for a token exchange failure', async () => {
vi.stubGlobal('window', {
location: { pathname: '/', search: '?oidcError=OIDC_TOKEN_EXCHANGE_FAILED', hash: '' },
history: { replaceState: vi.fn() },
});
const { consumeOIDCCallbackError } = await import('./oidc');
expect(consumeOIDCCallbackError()).toEqual({
code: 'OIDC_TOKEN_EXCHANGE_FAILED',
message: '统一认证登录结果无效,请重新登录',
retryable: false,
});
});
});