1
0
forked from boos/BoosAPI

fix oauth route unit delete rollback (#450)

This commit is contained in:
Cita
2026-04-09 11:15:57 +08:00
committed by GitHub
parent 569a15e14c
commit 03bebae17f
2 changed files with 121 additions and 0 deletions
+91
View File
@@ -3246,6 +3246,97 @@ describe('oauth routes', { timeout: 15_000 }, () => {
} }
}); });
it('restores oauth route unit route channels when delete rebuild keeps failing', async () => {
const site = await db.insert(schema.sites).values({
name: 'ChatGPT Codex OAuth',
url: 'https://chatgpt.com/backend-api/codex',
platform: 'codex',
status: 'active',
}).returning().get();
const first = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'rollback-delete-double-fail-a@example.com',
accessToken: 'rollback-delete-double-fail-access-a',
status: 'active',
oauthProvider: 'codex',
oauthAccountKey: 'rollback-delete-double-fail-a',
extraConfig: JSON.stringify({
oauth: {
provider: 'codex',
accountId: 'rollback-delete-double-fail-a',
accountKey: 'rollback-delete-double-fail-a',
email: 'rollback-delete-double-fail-a@example.com',
refreshToken: 'rollback-delete-double-fail-refresh-a',
},
}),
}).returning().get();
const second = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'rollback-delete-double-fail-b@example.com',
accessToken: 'rollback-delete-double-fail-access-b',
status: 'active',
oauthProvider: 'codex',
oauthAccountKey: 'rollback-delete-double-fail-b',
extraConfig: JSON.stringify({
oauth: {
provider: 'codex',
accountId: 'rollback-delete-double-fail-b',
accountKey: 'rollback-delete-double-fail-b',
email: 'rollback-delete-double-fail-b@example.com',
refreshToken: 'rollback-delete-double-fail-refresh-b',
},
}),
}).returning().get();
await db.insert(schema.modelAvailability).values([
{ accountId: first.id, modelName: 'gpt-5.4', available: true },
{ accountId: second.id, modelName: 'gpt-5.4', available: true },
]).run();
const routeRefreshWorkflow = await import('../../services/routeRefreshWorkflow.js');
await routeRefreshWorkflow.rebuildRoutesOnly();
const createResponse = await app.inject({
method: 'POST',
url: '/api/oauth/route-units',
payload: {
accountIds: [first.id, second.id],
name: 'Rollback Delete Double Fail Pool',
strategy: 'round_robin',
},
});
expect(createResponse.statusCode).toBe(200);
const routeUnitId = createResponse.json().routeUnit?.id as number;
const rebuildSpy = vi.spyOn(routeRefreshWorkflow, 'rebuildRoutesOnly');
rebuildSpy.mockImplementation(async () => {
throw new Error('route rebuild failed');
});
try {
const response = await app.inject({
method: 'DELETE',
url: `/api/oauth/route-units/${routeUnitId}`,
});
expect(response.statusCode).toBe(500);
expect(response.json()).toMatchObject({
message: 'route rebuild failed',
});
const routeUnits = await db.select().from(schema.oauthRouteUnits).all();
expect(routeUnits).toHaveLength(1);
const members = await db.select().from(schema.oauthRouteUnitMembers).all();
expect(members).toHaveLength(2);
const routeChannels = await db.select().from(schema.routeChannels).all();
expect(routeChannels).toHaveLength(1);
expect(routeChannels[0]?.oauthRouteUnitId).toBe(routeUnitId);
} finally {
rebuildSpy.mockRestore();
}
});
it('accepts normalized route unit strategy values from the HTTP payload', async () => { it('accepts normalized route unit strategy values from the HTTP payload', async () => {
const site = await db.insert(schema.sites).values({ const site = await db.insert(schema.sites).values({
name: 'ChatGPT Codex OAuth', name: 'ChatGPT Codex OAuth',
@@ -153,6 +153,7 @@ async function rollbackCreatedOauthRouteUnit(routeUnitId: number): Promise<void>
async function restoreDeletedOauthRouteUnit(snapshot: { async function restoreDeletedOauthRouteUnit(snapshot: {
unit: typeof schema.oauthRouteUnits.$inferSelect; unit: typeof schema.oauthRouteUnits.$inferSelect;
members: Array<typeof schema.oauthRouteUnitMembers.$inferSelect>; members: Array<typeof schema.oauthRouteUnitMembers.$inferSelect>;
channels: Array<typeof schema.routeChannels.$inferSelect>;
}): Promise<void> { }): Promise<void> {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
await tx.insert(schema.oauthRouteUnits).values({ await tx.insert(schema.oauthRouteUnits).values({
@@ -176,6 +177,31 @@ async function restoreDeletedOauthRouteUnit(snapshot: {
updatedAt: member.updatedAt, updatedAt: member.updatedAt,
}))).run(); }))).run();
} }
if (snapshot.channels.length > 0) {
await tx.insert(schema.routeChannels).values(snapshot.channels.map((channel) => ({
id: channel.id,
routeId: channel.routeId,
accountId: channel.accountId,
tokenId: channel.tokenId,
oauthRouteUnitId: channel.oauthRouteUnitId,
sourceModel: channel.sourceModel,
priority: channel.priority,
weight: channel.weight,
enabled: channel.enabled,
manualOverride: channel.manualOverride,
successCount: channel.successCount,
failCount: channel.failCount,
totalLatencyMs: channel.totalLatencyMs,
totalCost: channel.totalCost,
lastUsedAt: channel.lastUsedAt,
lastSelectedAt: channel.lastSelectedAt,
lastFailAt: channel.lastFailAt,
consecutiveFailCount: channel.consecutiveFailCount,
cooldownLevel: channel.cooldownLevel,
cooldownUntil: channel.cooldownUntil,
}))).run();
}
}); });
} }
@@ -373,6 +399,9 @@ export async function deleteOauthRouteUnit(routeUnitId: number) {
const existingMembers = await db.select().from(schema.oauthRouteUnitMembers) const existingMembers = await db.select().from(schema.oauthRouteUnitMembers)
.where(eq(schema.oauthRouteUnitMembers.unitId, routeUnitId)) .where(eq(schema.oauthRouteUnitMembers.unitId, routeUnitId))
.all(); .all();
const existingChannels = await db.select().from(schema.routeChannels)
.where(eq(schema.routeChannels.oauthRouteUnitId, routeUnitId))
.all();
await db.delete(schema.routeChannels) await db.delete(schema.routeChannels)
.where(eq(schema.routeChannels.oauthRouteUnitId, routeUnitId)) .where(eq(schema.routeChannels.oauthRouteUnitId, routeUnitId))
@@ -390,6 +419,7 @@ export async function deleteOauthRouteUnit(routeUnitId: number) {
await restoreDeletedOauthRouteUnit({ await restoreDeletedOauthRouteUnit({
unit: existing, unit: existing,
members: existingMembers, members: existingMembers,
channels: existingChannels,
}); });
try { try {
await routeRefreshWorkflow.rebuildRoutesOnly(); await routeRefreshWorkflow.rebuildRoutesOnly();