166 lines
6.2 KiB
TypeScript
166 lines
6.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import { WechatClient, type WechatFetch } from '../src/auth/wechat-client';
|
|
import { ApiException } from '../src/common/api-exception';
|
|
import { testConfig } from './test-config';
|
|
|
|
const responding =
|
|
(payload: unknown, status = 200): WechatFetch =>
|
|
async () =>
|
|
new Response(JSON.stringify(payload), {
|
|
status,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
test('微信兑换只向固定地址发送服务端凭据,忽略 session_key,不重试一次性 code', async () => {
|
|
let calls = 0;
|
|
const http: WechatFetch = async (input, init) => {
|
|
calls += 1;
|
|
const url = new URL(String(input));
|
|
assert.equal(url.origin, 'https://api.weixin.qq.com');
|
|
assert.equal(url.pathname, '/sns/jscode2session');
|
|
assert.equal(url.searchParams.get('appid'), testConfig.wechatMiniAppId);
|
|
assert.equal(
|
|
url.searchParams.get('secret'),
|
|
testConfig.wechatMiniAppSecret,
|
|
);
|
|
assert.equal(url.searchParams.get('js_code'), 'once-code');
|
|
assert.equal(init?.redirect, 'error');
|
|
assert.ok(init?.signal);
|
|
return responding({
|
|
openid: 'OpenIdA',
|
|
unionid: 'UnionA',
|
|
session_key: 'private-session-key',
|
|
})(input, init);
|
|
};
|
|
assert.deepEqual(
|
|
await new WechatClient(testConfig, http).exchangeCode('once-code'),
|
|
{ openid: 'OpenIdA', unionid: 'UnionA' },
|
|
);
|
|
assert.equal(calls, 1);
|
|
for (const unionid of [undefined, null, '']) {
|
|
const result = await new WechatClient(
|
|
testConfig,
|
|
responding({ openid: 'A', unionid, errcode: 0 }),
|
|
).exchangeCode('code');
|
|
assert.equal(result.unionid, null);
|
|
}
|
|
});
|
|
|
|
test('微信错误、畸形响应和网络失败映射为有限安全错误,不传播原始消息', async () => {
|
|
for (const [payload, status, reason] of [
|
|
[{ errcode: 40029 }, 400, 'WECHAT_CODE_INVALID'],
|
|
[{ errcode: 40163 }, 400, 'WECHAT_CODE_INVALID'],
|
|
[{ errcode: 40226 }, 403, 'WECHAT_LOGIN_BLOCKED'],
|
|
[{ errcode: 45011 }, 429, 'RATE_LIMITED'],
|
|
[{ errcode: -1 }, 503, 'AUTH_UPSTREAM_UNAVAILABLE'],
|
|
[
|
|
{ errcode: 40013, errmsg: 'private-secret' },
|
|
503,
|
|
'AUTH_UPSTREAM_UNAVAILABLE',
|
|
],
|
|
[{ openid: '' }, 503, 'AUTH_UPSTREAM_UNAVAILABLE'],
|
|
[{ openid: 'A', unionid: 123 }, 503, 'AUTH_UPSTREAM_UNAVAILABLE'],
|
|
[{ openid: 'x'.repeat(129) }, 503, 'AUTH_UPSTREAM_UNAVAILABLE'],
|
|
[{ openid: ' A' }, 503, 'AUTH_UPSTREAM_UNAVAILABLE'],
|
|
[null, 503, 'AUTH_UPSTREAM_UNAVAILABLE'],
|
|
[[], 503, 'AUTH_UPSTREAM_UNAVAILABLE'],
|
|
] as const) {
|
|
await assert.rejects(
|
|
new WechatClient(testConfig, responding(payload)).exchangeCode(
|
|
'code',
|
|
),
|
|
(error: unknown) => {
|
|
assert.ok(error instanceof ApiException);
|
|
assert.equal(error.getStatus(), status);
|
|
assert.equal(error.reason, reason);
|
|
assert.ok(
|
|
!JSON.stringify(error.getResponse()).includes(
|
|
'private-secret',
|
|
),
|
|
);
|
|
return true;
|
|
},
|
|
);
|
|
}
|
|
let calls = 0;
|
|
const failed: WechatFetch = async () => {
|
|
calls += 1;
|
|
throw new Error('https://api.weixin.qq.com?secret=private-secret');
|
|
};
|
|
await assert.rejects(
|
|
new WechatClient(testConfig, failed).exchangeCode('code'),
|
|
(error: unknown) =>
|
|
error instanceof ApiException &&
|
|
error.reason === 'AUTH_UPSTREAM_UNAVAILABLE',
|
|
);
|
|
assert.equal(calls, 1);
|
|
for (const http of [
|
|
responding({}, 502),
|
|
(async () => new Response('not-json')) as WechatFetch,
|
|
])
|
|
await assert.rejects(
|
|
new WechatClient(testConfig, http).exchangeCode('code'),
|
|
(error: unknown) =>
|
|
error instanceof ApiException &&
|
|
error.reason === 'AUTH_UPSTREAM_UNAVAILABLE',
|
|
);
|
|
});
|
|
|
|
test('微信白名单拒绝记录有限错误码和出口 IP,可按 requestId 排查且不泄漏原文', async () => {
|
|
const diagnostics: unknown[] = [];
|
|
const http = responding({
|
|
errcode: 40164,
|
|
errmsg: 'invalid ip 98.98.125.169, not in whitelist secret=private-secret',
|
|
session_key: 'private-session-key',
|
|
});
|
|
await assert.rejects(
|
|
new WechatClient(testConfig, http, (entry) =>
|
|
diagnostics.push(entry),
|
|
).exchangeCode('private-once-code', 'req_diagnostic'),
|
|
(error: unknown) =>
|
|
error instanceof ApiException &&
|
|
error.getStatus() === 503 &&
|
|
error.reason === 'AUTH_UPSTREAM_UNAVAILABLE',
|
|
);
|
|
assert.deepEqual(diagnostics, [
|
|
{
|
|
request_id: 'req_diagnostic',
|
|
failure_type: 'wechat_error',
|
|
http_status: 200,
|
|
wechat_errcode: 40164,
|
|
egress_ip: '98.98.125.169',
|
|
},
|
|
]);
|
|
const output = JSON.stringify(diagnostics);
|
|
for (const value of [
|
|
'private-secret',
|
|
'private-session-key',
|
|
'private-once-code',
|
|
testConfig.wechatMiniAppSecret,
|
|
])
|
|
assert.ok(!output.includes(value));
|
|
});
|
|
|
|
test('网络、HTTP 和响应解析诊断不携带原异常或微信响应', async () => {
|
|
const diagnostics: unknown[] = [];
|
|
for (const http of [
|
|
(async () => {
|
|
throw new Error('https://api.weixin.qq.com?secret=private-secret');
|
|
}) as WechatFetch,
|
|
responding({}, 502),
|
|
(async () => new Response('private-invalid-json')) as WechatFetch,
|
|
])
|
|
await assert.rejects(
|
|
new WechatClient(testConfig, http, (entry) =>
|
|
diagnostics.push(entry),
|
|
).exchangeCode('private-code'),
|
|
);
|
|
assert.deepEqual(diagnostics, [
|
|
{ request_id: undefined, failure_type: 'network' },
|
|
{ request_id: undefined, failure_type: 'http', http_status: 502 },
|
|
{ request_id: undefined, failure_type: 'json', http_status: 200 },
|
|
]);
|
|
assert.ok(!JSON.stringify(diagnostics).includes('private-'));
|
|
});
|