fix: 支持 SOCKS5 系统代理并修复脱敏令牌重复同步 (#154)
* fix: 支持 SOCKS5 系统代理并修复脱敏令牌重复同步 * fix: 避免复用完全模糊的脱敏令牌 * fix: 修复 reviewer 反馈的令牌同步与 SOCKS5 问题 * fix: avoid masked token sync collisions --------- Co-authored-by: Brucents <mail@urlk.cn> Co-authored-by: cita <juricek.chen@gmail.com>
This commit is contained in:
@@ -12,6 +12,8 @@ const createApiTokenMock = vi.fn();
|
||||
const getUserGroupsMock = vi.fn();
|
||||
const deleteApiTokenMock = vi.fn();
|
||||
|
||||
type AccountTokenServiceModule = typeof import('../../services/accountTokenService.js');
|
||||
|
||||
vi.mock('../../services/platforms/index.js', () => ({
|
||||
getAdapter: () => ({
|
||||
getApiTokens: (...args: unknown[]) => getApiTokensMock(...args),
|
||||
@@ -28,6 +30,7 @@ describe('account tokens sync routes with site status', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: DbModule['db'];
|
||||
let schema: DbModule['schema'];
|
||||
let maskToken: AccountTokenServiceModule['maskToken'];
|
||||
let dataDir = '';
|
||||
let seedId = 0;
|
||||
|
||||
@@ -63,9 +66,11 @@ describe('account tokens sync routes with site status', () => {
|
||||
|
||||
await import('../../db/migrate.js');
|
||||
const dbModule = await import('../../db/index.js');
|
||||
const accountTokenServiceModule = await import('../../services/accountTokenService.js');
|
||||
const routesModule = await import('./accountTokens.js');
|
||||
db = dbModule.db;
|
||||
schema = dbModule.schema;
|
||||
maskToken = accountTokenServiceModule.maskToken;
|
||||
|
||||
app = Fastify();
|
||||
await app.register(routesModule.accountTokensRoutes);
|
||||
@@ -196,6 +201,247 @@ describe('account tokens sync routes with site status', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('reuses an existing ready token when upstream only returns the matching masked token', async () => {
|
||||
const { account } = await seedAccount({ siteStatus: 'active' });
|
||||
const fullToken = 'sk-real-token-1234';
|
||||
await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'masked-only',
|
||||
token: fullToken,
|
||||
source: 'manual',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
tokenGroup: 'default',
|
||||
valueStatus: 'ready' as any,
|
||||
}).run();
|
||||
|
||||
getApiTokensMock.mockResolvedValue([
|
||||
{ name: 'masked-only', key: maskToken(fullToken), enabled: true, tokenGroup: 'default' },
|
||||
]);
|
||||
getApiTokenMock.mockResolvedValue(null);
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/account-tokens/sync/${account.id}`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
synced: true,
|
||||
status: 'synced',
|
||||
created: 0,
|
||||
updated: 1,
|
||||
maskedPending: 0,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
const tokenRows = await db.select()
|
||||
.from(schema.accountTokens)
|
||||
.where(eq(schema.accountTokens.accountId, account.id))
|
||||
.all();
|
||||
expect(tokenRows).toHaveLength(1);
|
||||
expect(tokenRows[0]).toMatchObject({
|
||||
name: 'masked-only',
|
||||
token: fullToken,
|
||||
source: 'sync',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
tokenGroup: 'default',
|
||||
});
|
||||
expect((tokenRows[0] as any).valueStatus).toBe('ready');
|
||||
});
|
||||
|
||||
it('removes matching masked_pending placeholders after reusing a ready token', async () => {
|
||||
const { account } = await seedAccount({ siteStatus: 'active' });
|
||||
const fullToken = 'sk-real-token-1234';
|
||||
const maskedToken = maskToken(fullToken);
|
||||
|
||||
await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'masked-only',
|
||||
token: fullToken,
|
||||
source: 'manual',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
tokenGroup: 'default',
|
||||
valueStatus: 'ready' as any,
|
||||
}).run();
|
||||
await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'masked-only',
|
||||
token: maskedToken,
|
||||
source: 'sync',
|
||||
enabled: false,
|
||||
isDefault: false,
|
||||
tokenGroup: 'default',
|
||||
valueStatus: 'masked_pending' as any,
|
||||
}).run();
|
||||
|
||||
getApiTokensMock.mockResolvedValue([
|
||||
{ name: 'masked-only', key: maskedToken, enabled: true, tokenGroup: 'default' },
|
||||
]);
|
||||
getApiTokenMock.mockResolvedValue(null);
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/account-tokens/sync/${account.id}`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
synced: true,
|
||||
status: 'synced',
|
||||
created: 0,
|
||||
updated: 1,
|
||||
maskedPending: 0,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
const tokenRows = await db.select()
|
||||
.from(schema.accountTokens)
|
||||
.where(eq(schema.accountTokens.accountId, account.id))
|
||||
.all();
|
||||
expect(tokenRows).toHaveLength(1);
|
||||
expect(tokenRows[0]).toMatchObject({
|
||||
name: 'masked-only',
|
||||
token: fullToken,
|
||||
source: 'sync',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
tokenGroup: 'default',
|
||||
});
|
||||
expect((tokenRows[0] as any).valueStatus).toBe('ready');
|
||||
});
|
||||
|
||||
it('does not reuse a different ready token when another logical token shares the same masked value', async () => {
|
||||
const { account } = await seedAccount({ siteStatus: 'active' });
|
||||
const firstFullToken = 'sk-real-token-1234';
|
||||
const secondFullToken = 'sk-real-zzzzz-1234';
|
||||
const sharedMaskedToken = maskToken(firstFullToken);
|
||||
|
||||
expect(maskToken(secondFullToken)).toBe(sharedMaskedToken);
|
||||
|
||||
await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'first-token',
|
||||
token: firstFullToken,
|
||||
source: 'manual',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
tokenGroup: 'default',
|
||||
valueStatus: 'ready' as any,
|
||||
}).run();
|
||||
await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'second-token',
|
||||
token: sharedMaskedToken,
|
||||
source: 'sync',
|
||||
enabled: false,
|
||||
isDefault: false,
|
||||
tokenGroup: 'default',
|
||||
valueStatus: 'masked_pending' as any,
|
||||
}).run();
|
||||
|
||||
getApiTokensMock.mockResolvedValue([
|
||||
{ name: 'second-token', key: sharedMaskedToken, enabled: true, tokenGroup: 'default' },
|
||||
]);
|
||||
getApiTokenMock.mockResolvedValue(null);
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/account-tokens/sync/${account.id}`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
synced: true,
|
||||
status: 'synced',
|
||||
created: 0,
|
||||
updated: 1,
|
||||
maskedPending: 1,
|
||||
total: 2,
|
||||
});
|
||||
|
||||
const tokenRows = await db.select()
|
||||
.from(schema.accountTokens)
|
||||
.where(eq(schema.accountTokens.accountId, account.id))
|
||||
.all();
|
||||
expect(tokenRows).toHaveLength(2);
|
||||
expect(tokenRows.find((row) => row.token === firstFullToken)).toMatchObject({
|
||||
name: 'first-token',
|
||||
token: firstFullToken,
|
||||
source: 'manual',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
tokenGroup: 'default',
|
||||
});
|
||||
const maskedRow = tokenRows.find((row) => row.name === 'second-token');
|
||||
expect(maskedRow).toMatchObject({
|
||||
token: sharedMaskedToken,
|
||||
source: 'sync',
|
||||
enabled: false,
|
||||
isDefault: false,
|
||||
tokenGroup: 'default',
|
||||
});
|
||||
expect((maskedRow as any)?.valueStatus).toBe('masked_pending');
|
||||
});
|
||||
|
||||
it('keeps fully ambiguous short masks as masked_pending instead of reusing a ready token', async () => {
|
||||
const { account } = await seedAccount({ siteStatus: 'active' });
|
||||
const fullToken = 'sk-abcd';
|
||||
await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'short-token',
|
||||
token: fullToken,
|
||||
source: 'manual',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
tokenGroup: 'default',
|
||||
valueStatus: 'ready' as any,
|
||||
}).run();
|
||||
|
||||
getApiTokensMock.mockResolvedValue([
|
||||
{ name: 'short-token', key: maskToken(fullToken), enabled: true, tokenGroup: 'default' },
|
||||
]);
|
||||
getApiTokenMock.mockResolvedValue(null);
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/account-tokens/sync/${account.id}`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
synced: true,
|
||||
status: 'synced',
|
||||
reason: 'upstream_masked_tokens',
|
||||
created: 1,
|
||||
updated: 0,
|
||||
maskedPending: 1,
|
||||
total: 2,
|
||||
});
|
||||
|
||||
const tokenRows = await db.select()
|
||||
.from(schema.accountTokens)
|
||||
.where(eq(schema.accountTokens.accountId, account.id))
|
||||
.all();
|
||||
expect(tokenRows).toHaveLength(2);
|
||||
expect(tokenRows.find((row) => row.token === fullToken)).toBeDefined();
|
||||
const maskedRow = tokenRows.find((row) => row.token === 'sk-***');
|
||||
expect(maskedRow).toMatchObject({
|
||||
name: 'short-token',
|
||||
source: 'sync',
|
||||
enabled: false,
|
||||
isDefault: false,
|
||||
tokenGroup: 'default',
|
||||
});
|
||||
expect((maskedRow as any)?.valueStatus).toBe('masked_pending');
|
||||
});
|
||||
|
||||
it('rejects sync and token management for apikey connections', async () => {
|
||||
const { account } = await seedAccount({ siteStatus: 'active', accessToken: '' });
|
||||
await db.update(schema.accounts)
|
||||
|
||||
Reference in New Issue
Block a user