Files
BPOS-Service-Gateway/test/integration/proxy.test.ts
T

90 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { gzipSync } from 'node:zlib';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { buildApp } from '../../src/app/app';
import { makeToken, setupTestJwks } from '../support/token';
// C5:路由的目标地址取自 config,而 config 在模块求值时一次性读 env——
// 注入必须早于 import(vi.hoisted 会被提升到所有 import 之前)。
vi.hoisted(() => {
process.env.TASK_SERVICE_URL = 'http://127.0.0.1:18811';
process.env.DATA_SERVICE_URL = 'http://127.0.0.1:1'; // 保证连不上:验 502
});
let downstream: ReturnType<typeof serve>;
let captured: { auth?: string; fake?: string; path?: string } = {};
describe('gateway proxy', () => {
let token = '';
beforeAll(async () => {
await setupTestJwks();
token = await makeToken({ roles: ['user'], aud: 'api-gateway' });
const echo = new Hono();
// 下游压缩响应:undici 会替网关解码 body,content-encoding 不能原样回传(D9)
echo.get('/gzip', () => {
const body = gzipSync(JSON.stringify({ from: 'downstream' }));
return new Response(body, {
headers: {
'content-encoding': 'gzip',
'content-type': 'application/json',
'content-length': String(body.byteLength),
},
});
});
echo.all('*', (c) => {
captured = {
auth: c.req.header('authorization') ?? '',
fake: c.req.header('x-user-id') ?? '',
path: c.req.path,
};
return c.json({ from: 'downstream' });
});
// D11:等 listening 回调再往下走,否则首个用例可能打在还没起来的端口上
downstream = await new Promise<ReturnType<typeof serve>>((resolve) => {
const s = serve({ fetch: echo.fetch, port: 18811 }, () => resolve(s));
});
});
afterAll(() => {
downstream.close();
});
it('无 token → 401,不打扰下游', async () => {
captured = {};
const res = await buildApp().request('/api/task/task/t-1/view');
expect(res.status).toBe(401);
expect(captured.path).toBeUndefined();
});
it('带 token 转发:前缀剥除、Authorization 原样、伪造内部头被剥', async () => {
const res = await buildApp().request('/api/task/task/t-1/view', {
headers: { authorization: `Bearer ${token}`, 'x-user-id': 'forged-user' },
});
expect(res.status).toBe(200);
expect(captured.path).toBe('/task/t-1/view');
expect(captured.auth).toBe(`Bearer ${token}`);
expect(captured.fake).toBe('');
});
it('下游不可达 → 502 统一错误形状,点名是哪个下游(C3)', async () => {
const res = await buildApp().request('/api/data/ledger/domains', {
headers: { authorization: `Bearer ${token}` },
});
expect(res.status).toBe(502);
const body = (await res.json()) as { error: string; message: string; hint: string };
expect(body.error).toBe('bad_gateway');
expect(body.message).toContain('http://127.0.0.1:1');
expect(body.hint).toBeTruthy();
});
it('下游 gzip 响应:解码后的 body 不得再带 content-encoding(D9)', async () => {
const res = await buildApp().request('/api/task/gzip', {
headers: { authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
expect(res.headers.get('content-encoding')).toBeNull();
expect(res.headers.get('content-length')).toBeNull();
expect(await res.json()).toEqual({ from: 'downstream' });
});
});