Files
metapi/src/server/routes/api/accountTokens.batch.test.ts
T

176 lines
4.8 KiB
TypeScript

import Fastify, { type FastifyInstance } from 'fastify';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { mkdtempSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { eq } from 'drizzle-orm';
const { deleteApiTokenMock } = vi.hoisted(() => ({
deleteApiTokenMock: vi.fn(),
}));
vi.mock('../../services/platforms/index.js', () => ({
getAdapter: vi.fn(() => ({
deleteApiToken: deleteApiTokenMock,
})),
}));
type DbModule = typeof import('../../db/index.js');
describe('account token batch routes', () => {
let app: FastifyInstance;
let db: DbModule['db'];
let schema: DbModule['schema'];
let dataDir = '';
beforeAll(async () => {
dataDir = mkdtempSync(join(tmpdir(), 'metapi-account-tokens-batch-'));
process.env.DATA_DIR = dataDir;
await import('../../db/migrate.js');
const dbModule = await import('../../db/index.js');
const routesModule = await import('./accountTokens.js');
db = dbModule.db;
schema = dbModule.schema;
app = Fastify();
await app.register(routesModule.accountTokensRoutes);
});
beforeEach(async () => {
deleteApiTokenMock.mockReset();
deleteApiTokenMock.mockResolvedValue(true);
await db.delete(schema.accountTokens).run();
await db.delete(schema.accounts).run();
await db.delete(schema.sites).run();
await db.insert(schema.sites).values({
id: 1,
name: 'site-1',
url: 'https://site-1.example.com',
platform: 'new-api',
status: 'active',
}).run();
await db.insert(schema.accounts).values({
id: 1,
siteId: 1,
username: 'alpha',
accessToken: 'session-alpha',
status: 'active',
}).run();
await db.insert(schema.accountTokens).values([
{
id: 1,
accountId: 1,
name: 'token-1',
token: 'sk-token-1',
enabled: false,
},
{
id: 2,
accountId: 1,
name: 'token-2',
token: 'sk-token-2',
enabled: false,
},
]).run();
});
afterAll(async () => {
await app.close();
delete process.env.DATA_DIR;
});
it('enables selected account tokens and reports failures', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/account-tokens/batch',
payload: {
ids: [1, 2, 999],
action: 'enable',
},
});
expect(response.statusCode).toBe(200);
const body = response.json() as {
successIds?: number[];
failedItems?: Array<{ id: number; message: string }>;
};
expect(body.successIds).toEqual([1, 2]);
expect(body.failedItems).toHaveLength(1);
expect(body.failedItems?.[0]?.id).toBe(999);
const rows = await db.select().from(schema.accountTokens).all();
expect(rows.every((row) => row.enabled === true)).toBe(true);
});
it('rejects enabling masked_pending placeholders until they are completed', async () => {
await db.update(schema.accountTokens)
.set({
enabled: false,
valueStatus: 'masked_pending' as any,
token: 'sk-mask***tail',
})
.where(eq(schema.accountTokens.id, 1))
.run();
const response = await app.inject({
method: 'POST',
url: '/api/account-tokens/batch',
payload: {
ids: [1, 2],
action: 'enable',
},
});
expect(response.statusCode).toBe(200);
const body = response.json() as {
successIds?: number[];
failedItems?: Array<{ id: number; message: string }>;
};
expect(body.successIds).toEqual([2]);
expect(body.failedItems).toEqual([
expect.objectContaining({
id: 1,
message: expect.stringContaining('待补全令牌'),
}),
]);
const rows = await db.select().from(schema.accountTokens).all();
expect(rows.find((row) => row.id === 1)?.enabled).toBe(false);
expect(rows.find((row) => row.id === 2)?.enabled).toBe(true);
});
it('deletes selected account tokens through the upstream adapter', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/account-tokens/batch',
payload: {
ids: [1],
action: 'delete',
},
});
expect(response.statusCode).toBe(200);
expect(deleteApiTokenMock).toHaveBeenCalledTimes(1);
const remaining = await db.select().from(schema.accountTokens).all();
expect(remaining.map((item) => item.id)).toEqual([2]);
});
it('rejects invalid account token batch action', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/account-tokens/batch',
payload: {
ids: [1],
action: 'nope',
},
});
expect(response.statusCode).toBe(400);
expect((response.json() as { message?: string }).message).toContain('action');
});
});