import 'reflect-metadata'; import assert from 'node:assert/strict'; import { test } from 'node:test'; import { Controller, Get } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import type { NestExpressApplication } from '@nestjs/platform-express'; import type { ApiSuccess, WechatLoginResult } from '@doodle/contracts'; import { AppModule } from '../src/app.module'; import { configureApplication } from '../src/app.setup'; import { AppLogger } from '../src/logging/app-logger'; import { PrismaService } from '../src/database/prisma.service'; import { WECHAT_FETCH, type WechatFetch } from '../src/auth/wechat-client'; import { Roles } from '../src/auth/auth.decorators'; import { JwtTokenService } from '../src/auth/jwt-token.service'; import { AccountRepository } from '../src/auth/account.repository'; import { testConfig } from './test-config'; @Controller('test-admin') class AdminProbe { @Get() @Roles('admin') get() { return { ok: true }; } } const databaseUrl = process.env.TEST_DATABASE_URL; test( '真实 PostgreSQL:微信登录、并发、本人隔离及账户状态鉴权', { skip: !databaseUrl }, async (context) => { // 即使手工运行测试,也只允许本机脚本创建的短期库,禁止误用开发/生产数据库。 const parsed = new URL(databaseUrl!); assert.ok(['127.0.0.1', 'localhost'].includes(parsed.hostname)); assert.match(parsed.pathname, /^\/doodle_auth_test_[a-f0-9]{32}$/); const config = { ...testConfig, databaseUrl: databaseUrl! }; const logs: string[] = []; const logger = new AppLogger('info', (line) => logs.push(line)); const requests: string[] = []; const http: WechatFetch = async (input) => { const code = new URL(String(input)).searchParams.get('js_code')!; requests.push(code); if (code === 'bad-code') return Response.json({ errcode: 40029, errmsg: 'private-upstream-message', }); if (code === 'risk-code') return Response.json({ errcode: 40226 }); if (code === 'quota-code') return Response.json({ errcode: 45011 }); if (code === 'network-code') throw new Error('private-network-secret'); return Response.json({ openid: code.startsWith('concurrent-') ? 'ConcurrentOpenID' : code, session_key: 'private-session-key', ...(code === 'LegacyOpenID' ? { unionid: 'FreshUnion' } : {}), }); }; const module = await Test.createTestingModule({ imports: [AppModule.forRoot(config)], controllers: [AdminProbe], }) .overrideProvider(WECHAT_FETCH) .useValue(http) .compile(); const app = module.createNestApplication({ logger, bodyParser: false, }); context.after(() => app.close()); configureApplication(app, config, logger); await app.listen(0, '127.0.0.1'); const origin = await app.getUrl(); const prisma = app.get(PrismaService).client; const tokens = app.get(JwtTokenService); const repository = app.get(AccountRepository); const request = (path: string, token?: string) => fetch(`${origin}/api/v1/${path}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, signal: AbortSignal.timeout(10_000), }); const login = (code: string, mode = 'start', extra: object = {}) => fetch(`${origin}/api/v1/auth/wechat/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Forwarded-For': 'untrusted-client-ip', }, body: JSON.stringify({ code, mode, ...extra }), signal: AbortSignal.timeout(15_000), }); const reason = async ( response: Response, status: number, expected: string, ) => { assert.equal(response.status, status); const body = (await response.json()) as { reason: string; requestId: string; }; assert.equal(body.reason, expected); assert.ok(body.requestId); }; const loginData = async (response: Response) => { assert.equal(response.status, 200); assert.equal(response.headers.get('cache-control'), 'no-store'); return ((await response.json()) as ApiSuccess) .data; }; await context.test('初始配置和只读就绪检查可用', async () => { assert.equal((await request('health/ready')).status, 200); assert.equal(await prisma.pageConfig.count(), 1); assert.equal(await prisma.setting.count(), 1); assert.equal(await prisma.user.count(), 0); await reason(await request('me'), 401, 'AUTH_REQUIRED'); }); await context.test( '首次静默登录默认普通用户,续期不改变版本,响应只含白名单', async () => { const data = await loginData(await login('FirstOpenID')); const claims = await tokens.verifyAccessToken(data.accessToken); assert.equal(data.user.id, claims.sub); assert.equal(data.expiresIn, 86400); assert.equal( data.expiresAt, new Date(claims.exp * 1000).toISOString(), ); assert.deepEqual( Object.keys(data.user).sort(), [ 'id', 'nickName', 'avatarUrl', 'avatarAssetId', 'avatarUrlExpiresAt', 'createdAt', ].sort(), ); const stored = await prisma.user.findUniqueOrThrow({ where: { id: data.user.id }, }); assert.equal(stored.role, 'user'); assert.equal(stored.status, 'active'); assert.equal(stored.authVersion, 1); const second = await loginData( await login('FirstOpenID', 'resume'), ); assert.equal(second.user.id, data.user.id); const me = await request( 'me?userId=somebody-else', data.accessToken, ); assert.equal(me.status, 200); assert.equal(me.headers.get('cache-control'), 'no-store'); assert.equal( ((await me.json()) as { data: { id: string } }).data.id, data.user.id, ); assert.equal( (await request('test-admin', data.accessToken)).status, 403, ); }, ); await context.test( '老 ID、资料、版本及时间保留,UnionID 只补空不覆盖', async () => { const createdAt = new Date('2025-01-01T00:00:00Z'); const updatedAt = new Date('2025-01-02T00:00:00Z'); await prisma.user.create({ data: { id: 'legacy_cloud_id', openid: 'LegacyOpenID', nickName: '老用户', authVersion: 3, createdAt, updatedAt, }, }); const data = await loginData( await login('LegacyOpenID', 'resume'), ); assert.equal(data.user.id, 'legacy_cloud_id'); assert.equal(data.user.nickName, '老用户'); const stored = await prisma.user.findUniqueOrThrow({ where: { id: 'legacy_cloud_id' }, }); assert.equal(stored.unionid, 'FreshUnion'); assert.equal(stored.authVersion, 3); assert.equal( stored.createdAt.toISOString(), createdAt.toISOString(), ); await repository.login( { openid: 'LegacyOpenID', unionid: 'ConflictingUnion' }, 'resume', ); await repository.login( { openid: 'LegacyOpenID', unionid: null }, 'resume', ); const after = await prisma.user.findUniqueOrThrow({ where: { id: 'legacy_cloud_id' }, }); assert.equal(after.unionid, 'FreshUnion'); assert.equal( after.updatedAt.toISOString(), stored.updatedAt.toISOString(), ); assert.ok(after.lastActiveAt > createdAt); }, ); await context.test( '并发首次登录只生成一个账户,OpenID 区分大小写', async () => { const responses = await Promise.all( Array.from({ length: 8 }, (_, i) => login(`concurrent-${i}`), ), ); const users = await Promise.all( responses.map(async (r) => (await loginData(r)).user.id), ); assert.equal(new Set(users).size, 1); assert.equal( await prisma.user.count({ where: { openid: 'ConcurrentOpenID' }, }), 1, ); const lower = await repository.login( { openid: 'concurrentopenid', unionid: null }, 'start', ); assert.notEqual(lower.account.id, users[0]); }, ); await context.test( 'resume 不建号,禁用不能被 start 重置,撤销/降权/删除即时生效', async () => { await reason( await login('MissingOpenID', 'resume'), 401, 'ACCOUNT_NOT_FOUND', ); assert.equal( await prisma.user.count({ where: { openid: 'MissingOpenID' }, }), 0, ); const data = await loginData(await login('LifecycleOpenID')); const id = data.user.id; await prisma.user.update({ where: { id }, data: { role: 'admin' }, }); assert.equal( (await request('test-admin', data.accessToken)).status, 200, ); await prisma.user.update({ where: { id }, data: { role: 'user' }, }); assert.equal( (await request('test-admin', data.accessToken)).status, 403, ); await prisma.user.update({ where: { id }, data: { authVersion: { increment: 1 } }, }); await reason( await request('me', data.accessToken), 401, 'AUTH_REVOKED', ); const renewed = await loginData( await login('LifecycleOpenID', 'resume'), ); await prisma.user.update({ where: { id }, data: { status: 'disabled' }, }); await reason( await request('me', renewed.accessToken), 401, 'ACCOUNT_DISABLED', ); await reason( await login('LifecycleOpenID'), 401, 'ACCOUNT_DISABLED', ); await prisma.user.delete({ where: { id } }); await reason( await request('me', renewed.accessToken), 401, 'ACCOUNT_NOT_FOUND', ); await reason( await login('LifecycleOpenID', 'resume'), 401, 'ACCOUNT_NOT_FOUND', ); const fresh = await loginData(await login('LifecycleOpenID')); assert.notEqual(fresh.user.id, id); }, ); await context.test( '拒绝额外身份参数和坏 code,错误不变成成功响应', async () => { const before = requests.length; for (const input of [ { openid: 'forged' }, { appid: 'forged' }, { userId: 'forged' }, { role: 'admin' }, ]) assert.equal( (await login('valid', 'start', input)).status, 400, ); assert.equal((await login(' ', 'start')).status, 400); assert.equal((await login('valid', 'unknown')).status, 400); assert.equal(requests.length, before); await reason( await login('bad-code'), 400, 'WECHAT_CODE_INVALID', ); await reason( await login('risk-code'), 403, 'WECHAT_LOGIN_BLOCKED', ); const quota = await login('quota-code'); assert.equal(quota.headers.get('retry-after'), '60'); await reason(quota, 429, 'RATE_LIMITED'); await reason( await login('network-code'), 503, 'AUTH_UPSTREAM_UNAVAILABLE', ); }, ); await context.test( '认证失败原因及日志不泄露凭据,伪造转发头不能绕过限流', async () => { await reason( await request('me', 'invalid-token'), 401, 'AUTH_INVALID', ); const jwt = app.get(JwtTokenService); const expired = await app .get((await import('@nestjs/jwt')).JwtService) .signAsync( { sub: 'legacy_cloud_id', authVersion: 3 }, { secret: config.jwtSecret, issuer: config.jwtIssuer, audience: config.jwtAudience, expiresIn: -1, }, ); await reason(await request('me', expired), 401, 'AUTH_EXPIRED'); const token = await jwt.signAccessToken('legacy_cloud_id', 3); assert.equal((await request('me', token)).status, 200); let limited = false; for (let i = 0; i < 35; i++) { const result = await login(`rate-${i}`, 'resume'); if (result.status === 429) { assert.ok(result.headers.get('retry-after')); limited = true; break; } } assert.ok(limited); const allLogs = logs.join('\n'); for (const secret of [ config.wechatMiniAppSecret, 'private-session-key', 'private-upstream-message', 'private-network-secret', token, 'LegacyOpenID', ]) assert.ok(!allLogs.includes(secret)); }, ); }, );