Files
2026-07-13 12:30:28 +08:00

492 lines
16 KiB
TypeScript

import { beforeEach, describe, expect, test, vi } from 'vitest';
import nock from 'nock';
async function loadPinmeApi(env: Record<string, string>) {
vi.resetModules();
Object.assign(process.env, env);
return import('../../bin/utils/pinmeApi');
}
describe('pinmeApi', () => {
beforeEach(() => {
nock.cleanAll();
});
test('returns DNS domains as available without an API call', async () => {
const { checkDomainAvailable } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
await expect(checkDomainAvailable('example.com')).resolves.toEqual({
is_valid: true,
});
expect(nock.isDone()).toBe(true);
});
test('checks PinMe subdomain availability through configured endpoint', async () => {
const { checkDomainAvailable } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
PINME_CHECK_DOMAIN_PATH: '/check_domain',
});
nock('https://api.pinme.test')
.post('/check_domain', { domain_name: 'demo' })
.reply(200, { data: { is_valid: false, error: 'taken' } });
await expect(checkDomainAvailable('demo')).resolves.toEqual({
is_valid: false,
error: 'taken',
});
});
test('defaults subdomain availability to true for unexpected successful shapes', async () => {
const { checkDomainAvailable } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
PINME_CHECK_DOMAIN_PATH: '/check_domain',
});
nock('https://api.pinme.test')
.post('/check_domain', { domain_name: 'demo' })
.reply(200, { code: 200, data: { unexpected: true } });
await expect(checkDomainAvailable('demo')).resolves.toEqual({
is_valid: true,
});
});
test('checks top-level domain availability and surfaces recoverable HTTP failures', async () => {
const { checkDomainAvailable } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
PINME_CHECK_DOMAIN_PATH: '/check_domain',
});
nock('https://api.pinme.test')
.post('/check_domain', { domain_name: 'direct' })
.reply(200, { is_valid: false, error: 'reserved' })
.post('/check_domain', { domain_name: 'missing' })
.reply(404, { message: 'not found' });
await expect(checkDomainAvailable('direct')).resolves.toEqual({
is_valid: false,
error: 'reserved',
});
await expect(checkDomainAvailable('missing')).rejects.toThrow('not found');
});
test('caches root domain until force refresh', async () => {
const { getRootDomain } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.get('/root_domain')
.reply(200, { code: 200, data: { domain: 'first.pinme.test' } })
.get('/root_domain')
.reply(200, { code: 200, data: { domain: 'second.pinme.test' } });
await expect(getRootDomain()).resolves.toBe('first.pinme.test');
await expect(getRootDomain()).resolves.toBe('first.pinme.test');
await expect(getRootDomain(true)).resolves.toBe('second.pinme.test');
});
test('getRootDomain rejects successful responses without a domain', async () => {
const { getRootDomain } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.get('/root_domain')
.reply(200, { code: 200, msg: 'domain missing', data: {} });
await expect(getRootDomain(true)).rejects.toThrow('domain missing');
});
test('binds anonymous devices and returns false on token expiration', async () => {
const { bindAnonymousDevice } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.post('/bind_anonymous', { anonymous_uid: 'anon-1' })
.reply(200, { code: 200 })
.post('/bind_anonymous', { anonymous_uid: 'anon-2' })
.reply(401, { message: 'token expired' });
await expect(bindAnonymousDevice('anon-1')).resolves.toBe(true);
await expect(bindAnonymousDevice('anon-2')).resolves.toBe(false);
});
test('bindAnonymousDevice returns false for non-token failures', async () => {
const { bindAnonymousDevice } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.post('/bind_anonymous', { anonymous_uid: 'anon-fail' })
.reply(500, { message: 'server exploded' });
await expect(bindAnonymousDevice('anon-fail')).resolves.toBe(false);
});
test('throws token expired for auth failures', async () => {
const { getMyDomains } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.get('/my_domains')
.reply(401, { message: 'token expired' });
await expect(getMyDomains()).rejects.toThrow('Token expired');
});
test('detects token expiration from business codes and localized messages', async () => {
const { checkDomainAvailable, getMyDomains } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
PINME_CHECK_DOMAIN_PATH: '/check_domain',
});
nock('https://api.pinme.test')
.post('/check_domain', { domain_name: 'demo' })
.reply(200, { code: 10001, msg: '登录已过期' })
.get('/my_domains')
.reply(200, { code: 'TOKEN_EXPIRED', msg: 'token expired' });
await expect(checkDomainAvailable('demo')).rejects.toThrow(
'Token expired',
);
await expect(getMyDomains()).rejects.toThrow('Token expired');
});
test('reads domain list variants and returns empty arrays for business failures', async () => {
const { getMyDomains } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.get('/my_domains')
.reply(200, {
code: 200,
data: {
list: [
{
domain_name: 'demo',
domain_type: 1,
bind_time: 1,
expire_time: 2,
},
],
},
})
.get('/my_domains')
.reply(200, { msg: 'missing code' });
await expect(getMyDomains()).resolves.toHaveLength(1);
await expect(getMyDomains()).resolves.toEqual([]);
});
test('reads array domain lists and treats business auth codes as expired', async () => {
const { getMyDomains } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.get('/my_domains')
.reply(200, {
code: 200,
data: [
{
domain_name: 'array-demo',
domain_type: 1,
bind_time: 1,
expire_time: 2,
},
],
})
.get('/my_domains')
.reply(200, { code: 403, msg: 'auth failed' });
await expect(getMyDomains()).resolves.toEqual([
expect.objectContaining({ domain_name: 'array-demo' }),
]);
await expect(getMyDomains()).rejects.toThrow('Token expired');
});
test('getMyDomains handles unsupported payloads and both auth business codes', async () => {
const { getMyDomains } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.get('/my_domains')
.reply(200, { code: 200, data: { list: 'not an array' } })
.get('/my_domains')
.reply(200, { code: 401, msg: 'auth failed' });
await expect(getMyDomains()).resolves.toEqual([]);
await expect(getMyDomains()).rejects.toThrow('Token expired');
});
test('binds DNS domains with auth headers', async () => {
const { bindDnsDomainV4 } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test', {
reqheaders: {
'x-auth-token': 'token',
'x-token-address': '0xabc',
},
})
.post('/bind_dns', { domain_name: 'example.com', hash: 'bafy' })
.reply(200, {
code: 200,
msg: 'ok',
data: { domain_name: 'example.com', hash: 'bafy' },
});
await expect(
bindDnsDomainV4('example.com', 'bafy', '0xabc', 'token'),
).resolves.toMatchObject({
code: 200,
data: { domain_name: 'example.com' },
});
});
test('binds PinMe subdomains and reports wallet balance and VIP status', async () => {
const { bindPinmeDomain, getWalletBalance, isVip } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.post('/bind_pinme_domain', {
domain_name: 'demo',
hash: 'bafy',
project_name: 'project',
})
.reply(200, { code: 200 })
.get('/pay/wallet/balance')
.reply(200, {
code: 200,
msg: 'ok',
data: { wallet_balance_usd: 12.5 },
})
.get('/is_vip')
.reply(200, {
code: 200,
msg: 'ok',
data: { is_vip: true },
});
await expect(bindPinmeDomain('demo', 'bafy', 'project')).resolves.toBe(true);
await expect(getWalletBalance('0xabc', 'token')).resolves.toMatchObject({
data: { wallet_balance_usd: 12.5 },
});
await expect(isVip('0xabc', 'token')).resolves.toMatchObject({
data: { is_vip: true },
});
});
test('bindPinmeDomain returns false for successful responses without code 200', async () => {
const { bindPinmeDomain } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.post('/bind_pinme_domain', {
domain_name: 'demo',
hash: 'bafy',
})
.reply(200, { msg: 'missing code' })
.post('/bind_pinme_domain', {
domain_name: 'project-demo',
hash: 'bafy-project',
project_name: 'project',
})
.reply(200, { code: 200, msg: 'ok' });
await expect(bindPinmeDomain('demo', 'bafy')).resolves.toBe(false);
await expect(
bindPinmeDomain('project-demo', 'bafy-project', 'project'),
).resolves.toBe(true);
});
test('sends account auth headers for wallet and VIP requests', async () => {
const { getWalletBalance, isVip } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test', {
reqheaders: {
'authentication-tokens': 'wallet-token',
'token-address': '0xwallet',
},
})
.get('/pay/wallet/balance')
.reply(200, { code: 200, msg: 'ok', data: { wallet_balance_usd: 1 } });
nock('https://api.pinme.test', {
reqheaders: {
'x-auth-token': 'vip-token',
'x-token-address': '0xvip',
},
})
.get('/is_vip')
.reply(200, { code: 200, msg: 'ok', data: { is_vip: false } });
await expect(getWalletBalance('0xwallet', 'wallet-token')).resolves.toEqual(
{
code: 200,
msg: 'ok',
data: { wallet_balance_usd: 1 },
},
);
await expect(isVip('0xvip', 'vip-token')).resolves.toEqual({
code: 200,
msg: 'ok',
data: { is_vip: false },
});
});
test('surfaces token expiration from bind and account APIs', async () => {
const { bindPinmeDomain, bindDnsDomainV4, getWalletBalance, isVip } =
await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
});
nock('https://api.pinme.test')
.post('/bind_pinme_domain')
.reply(403, { message: 'invalid token' })
.post('/bind_dns')
.reply(401, { message: 'token expired' })
.get('/pay/wallet/balance')
.reply(401, { message: 'unauthorized' })
.get('/is_vip')
.reply(401, { message: 'auth failed' });
await expect(bindPinmeDomain('demo', 'bafy')).rejects.toThrow(
'Token expired',
);
await expect(
bindDnsDomainV4('example.com', 'bafy', '0xabc', 'token'),
).rejects.toThrow('Token expired');
await expect(getWalletBalance('0xabc', 'token')).rejects.toThrow(
'Token expired',
);
await expect(isVip('0xabc', 'token')).rejects.toThrow('Token expired');
});
test('requests CAR export through the CAR API client', async () => {
const { requestCarExport } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
CAR_API_BASE: 'https://car.pinme.test',
});
nock('https://car.pinme.test')
.post('/car/export')
.query({ cid: 'bafy', uid: 'uid-1' })
.reply(200, {
code: 200,
msg: 'ok',
data: {
cid: 'bafy',
status: 'processing',
task_id: 'task-1',
},
});
await expect(requestCarExport('bafy', 'uid-1')).resolves.toEqual({
cid: 'bafy',
status: 'processing',
task_id: 'task-1',
});
});
test('checks CAR export status and normalizes API failures', async () => {
const { checkCarExportStatus, requestCarExport } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
CAR_API_BASE: 'https://car.pinme.test',
});
nock('https://car.pinme.test')
.get('/car/export/status')
.query({ task_id: 'task-1' })
.reply(200, {
code: 200,
msg: 'ok',
data: {
task_id: 'task-1',
cid: 'bafy',
status: 'completed',
download_url: 'https://download.pinme.test/file.car',
},
})
.post('/car/export')
.query({ cid: 'bad', uid: 'uid-1' })
.reply(200, { code: 500, msg: 'Export failed' });
await expect(checkCarExportStatus('task-1')).resolves.toMatchObject({
status: 'completed',
download_url: 'https://download.pinme.test/file.car',
});
await expect(requestCarExport('bad', 'uid-1')).rejects.toThrow(
/Failed to request CAR export: Export failed|Export failed/,
);
});
test('rejects CAR success codes that omit payload data', async () => {
const { checkCarExportStatus, requestCarExport } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
CAR_API_BASE: 'https://car.pinme.test',
});
nock('https://car.pinme.test')
.post('/car/export')
.query({ cid: 'empty', uid: 'uid-1' })
.reply(200, { code: 200, msg: 'missing export data' })
.get('/car/export/status')
.query({ task_id: 'empty-task' })
.reply(200, { code: 200, msg: 'missing status data' });
await expect(requestCarExport('empty', 'uid-1')).rejects.toThrow(
/missing export data/,
);
await expect(checkCarExportStatus('empty-task')).rejects.toThrow(
/missing status data/,
);
});
test('normalizes CAR status token expiration and response messages', async () => {
const { checkCarExportStatus } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
CAR_API_BASE: 'https://car.pinme.test',
});
nock('https://car.pinme.test')
.get('/car/export/status')
.query({ task_id: 'expired' })
.reply(401, { message: 'token expired' })
.get('/car/export/status')
.query({ task_id: 'failed' })
.reply(200, { code: 500, msg: 'Task failed' });
await expect(checkCarExportStatus('expired')).rejects.toThrow(
'Token expired',
);
await expect(checkCarExportStatus('failed')).rejects.toThrow(
/Failed to check export status: Task failed|Task failed/,
);
});
test('normalizes CAR HTTP response messages and generic failures', async () => {
const { checkCarExportStatus, requestCarExport } = await loadPinmeApi({
PINME_API_BASE: 'https://api.pinme.test',
CAR_API_BASE: 'https://car.pinme.test',
});
nock('https://car.pinme.test')
.post('/car/export')
.query({ cid: 'http-fail', uid: 'uid-1' })
.reply(503, { msg: 'CAR export unavailable' })
.post('/car/export')
.query({ cid: 'network-fail', uid: 'uid-1' })
.replyWithError('socket closed')
.get('/car/export/status')
.query({ task_id: 'http-fail' })
.reply(500, { msg: 'status unavailable' })
.get('/car/export/status')
.query({ task_id: 'network-fail' })
.replyWithError('status socket closed');
await expect(requestCarExport('http-fail', 'uid-1')).rejects.toThrow(
'CAR export unavailable',
);
await expect(requestCarExport('network-fail', 'uid-1')).rejects.toThrow(
/Failed to request CAR export: socket closed/,
);
await expect(checkCarExportStatus('http-fail')).rejects.toThrow(
'status unavailable',
);
await expect(checkCarExportStatus('network-fail')).rejects.toThrow(
/Failed to check export status: status socket closed/,
);
});
});