Skip to content

Commit 324f7bb

Browse files
James Mtendamemacursoragent
andcommitted
test(zoo-gateway): cover auth callback profile sync and sign-out
Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1a70573 commit 324f7bb

2 files changed

Lines changed: 236 additions & 0 deletions

File tree

src/core/webview/__tests__/ClineProvider.spec.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,14 @@ vi.mock("../../../api/providers/fetchers/modelCache", () => ({
230230
getModelsFromCache: vi.fn().mockReturnValue(undefined),
231231
}))
232232

233+
vi.mock("../../../services/zoo-code-auth", () => ({
234+
getZooCodeBaseUrl: vi.fn(() => "https://www.zoocode.dev"),
235+
getCachedZooCodeToken: vi.fn(),
236+
handleAuthCallback: vi.fn(),
237+
setZooCodeUserInfo: vi.fn(),
238+
disconnectZooCode: vi.fn(),
239+
}))
240+
233241
vi.mock("../../../shared/modes", () => ({
234242
modes: [
235243
{
@@ -3661,4 +3669,151 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
36613669
vi.mocked(fsUtils.fileExistsAtPath).mockRestore()
36623670
})
36633671
})
3672+
3673+
describe("Zoo Code auth profile sync", () => {
3674+
beforeEach(async () => {
3675+
const { getCachedZooCodeToken } = await import("../../../services/zoo-code-auth")
3676+
vi.mocked(getCachedZooCodeToken).mockReturnValue("")
3677+
})
3678+
3679+
describe("handleZooCodeCallback", () => {
3680+
it("creates a Zoo Gateway profile when none exists", async () => {
3681+
vi.spyOn(provider, "getState").mockResolvedValue({
3682+
apiConfiguration: { zooGatewayModelId: "anthropic/claude-sonnet-4" },
3683+
} as any)
3684+
vi.spyOn(provider.contextProxy, "getProviderSettings").mockReturnValue({
3685+
apiProvider: "anthropic",
3686+
} as any)
3687+
vi.spyOn(provider.contextProxy, "getValues").mockReturnValue({
3688+
currentApiConfigName: "Anthropic",
3689+
} as any)
3690+
const upsertSpy = vi.spyOn(provider, "upsertProviderProfile").mockResolvedValue("profile-id")
3691+
vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
3692+
;(provider as any).providerSettingsManager = {
3693+
listConfig: vi.fn().mockResolvedValue([]),
3694+
}
3695+
3696+
await provider.handleZooCodeCallback("zoo_ext_token")
3697+
3698+
expect(upsertSpy).toHaveBeenCalledWith(
3699+
"Zoo Gateway",
3700+
expect.objectContaining({
3701+
apiProvider: "zoo-gateway",
3702+
zooSessionToken: "zoo_ext_token",
3703+
zooGatewayBaseUrl: "https://www.zoocode.dev/api/gateway/v1",
3704+
}),
3705+
false,
3706+
)
3707+
})
3708+
3709+
it("updates every zoo-gateway profile and activates only the active one", async () => {
3710+
vi.spyOn(provider, "getState").mockResolvedValue({
3711+
apiConfiguration: { zooGatewayModelId: "anthropic/claude-sonnet-4" },
3712+
} as any)
3713+
vi.spyOn(provider.contextProxy, "getProviderSettings").mockReturnValue({
3714+
apiProvider: "zoo-gateway",
3715+
} as any)
3716+
vi.spyOn(provider.contextProxy, "getValues").mockReturnValue({
3717+
currentApiConfigName: "Zoo Gateway",
3718+
} as any)
3719+
const upsertSpy = vi.spyOn(provider, "upsertProviderProfile").mockResolvedValue("profile-id")
3720+
const saveConfig = vi.fn().mockResolvedValue(undefined)
3721+
vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
3722+
;(provider as any).providerSettingsManager = {
3723+
listConfig: vi.fn().mockResolvedValue([
3724+
{ name: "Zoo Gateway", apiProvider: "zoo-gateway" },
3725+
{ name: "Backup Zoo", apiProvider: "zoo-gateway" },
3726+
]),
3727+
getProfile: vi
3728+
.fn()
3729+
.mockResolvedValueOnce({
3730+
apiProvider: "zoo-gateway",
3731+
zooSessionToken: "old-token",
3732+
zooGatewayBaseUrl: "https://old.example/api/gateway/v1",
3733+
})
3734+
.mockResolvedValueOnce({
3735+
apiProvider: "zoo-gateway",
3736+
zooSessionToken: "old-token",
3737+
}),
3738+
saveConfig,
3739+
}
3740+
3741+
await provider.handleZooCodeCallback("new-token")
3742+
3743+
expect(upsertSpy).toHaveBeenCalledWith(
3744+
"Zoo Gateway",
3745+
expect.objectContaining({
3746+
zooSessionToken: "new-token",
3747+
zooGatewayBaseUrl: "https://www.zoocode.dev/api/gateway/v1",
3748+
}),
3749+
true,
3750+
)
3751+
expect(saveConfig).toHaveBeenCalledWith(
3752+
"Backup Zoo",
3753+
expect.objectContaining({
3754+
zooSessionToken: "new-token",
3755+
zooGatewayBaseUrl: "https://www.zoocode.dev/api/gateway/v1",
3756+
}),
3757+
)
3758+
})
3759+
3760+
it("logs and posts state when profile persistence fails", async () => {
3761+
vi.spyOn(provider, "getState").mockRejectedValue(new Error("state unavailable"))
3762+
vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
3763+
;(provider as any).providerSettingsManager = {
3764+
listConfig: vi.fn().mockResolvedValue([]),
3765+
}
3766+
3767+
await provider.handleZooCodeCallback("zoo_ext_token")
3768+
3769+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
3770+
expect.stringContaining("[handleZooCodeCallback] Failed to save zoo-gateway profile"),
3771+
)
3772+
})
3773+
})
3774+
3775+
describe("ensureZooGatewayProfileSeeded", () => {
3776+
it("does nothing when no cached auth token exists", async () => {
3777+
const handleSpy = vi.spyOn(provider, "handleZooCodeCallback").mockResolvedValue(undefined)
3778+
3779+
;(provider as any).providerSettingsManager = {
3780+
listConfig: vi.fn(),
3781+
}
3782+
3783+
await (provider as any).ensureZooGatewayProfileSeeded()
3784+
3785+
expect(handleSpy).not.toHaveBeenCalled()
3786+
})
3787+
3788+
it("skips seeding when every zoo-gateway profile already has the current token", async () => {
3789+
const { getCachedZooCodeToken } = await import("../../../services/zoo-code-auth")
3790+
vi.mocked(getCachedZooCodeToken).mockReturnValue("current-token")
3791+
const handleSpy = vi.spyOn(provider, "handleZooCodeCallback").mockResolvedValue(undefined)
3792+
3793+
;(provider as any).providerSettingsManager = {
3794+
listConfig: vi.fn().mockResolvedValue([{ name: "Zoo Gateway", apiProvider: "zoo-gateway" }]),
3795+
getProfile: vi.fn().mockResolvedValue({ zooSessionToken: "current-token" }),
3796+
}
3797+
3798+
await (provider as any).ensureZooGatewayProfileSeeded()
3799+
3800+
expect(handleSpy).not.toHaveBeenCalled()
3801+
})
3802+
3803+
it("re-seeds when any zoo-gateway profile has a stale or missing token", async () => {
3804+
const { getCachedZooCodeToken } = await import("../../../services/zoo-code-auth")
3805+
vi.mocked(getCachedZooCodeToken).mockReturnValue("fresh-token")
3806+
const handleSpy = vi.spyOn(provider, "handleZooCodeCallback").mockResolvedValue(undefined)
3807+
3808+
;(provider as any).providerSettingsManager = {
3809+
listConfig: vi.fn().mockResolvedValue([{ name: "Zoo Gateway", apiProvider: "zoo-gateway" }]),
3810+
getProfile: vi.fn().mockResolvedValue({ zooSessionToken: "stale-token" }),
3811+
}
3812+
3813+
await (provider as any).ensureZooGatewayProfileSeeded()
3814+
3815+
expect(handleSpy).toHaveBeenCalledWith("fresh-token")
3816+
})
3817+
})
3818+
})
36643819
})

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import type { Mock } from "vitest"
44

55
// Mock dependencies - must come before imports
66
vi.mock("../../../api/providers/fetchers/modelCache")
7+
vi.mock("../../../services/zoo-code-auth", () => ({
8+
disconnectZooCode: vi.fn().mockResolvedValue(undefined),
9+
}))
710
vi.mock("../../../api/providers/fetchers/lmstudio", () => ({
811
getLMStudioModels: vi.fn(),
912
}))
@@ -1146,3 +1149,81 @@ describe("webviewMessageHandler - downloadErrorDiagnostics", () => {
11461149
expect(generateErrorDiagnostics).not.toHaveBeenCalled()
11471150
})
11481151
})
1152+
1153+
describe("zooCodeSignOut", () => {
1154+
beforeEach(() => {
1155+
vi.clearAllMocks()
1156+
})
1157+
1158+
it("disconnects Zoo Code and clears tokens from all zoo-gateway profiles", async () => {
1159+
const { disconnectZooCode } = await import("../../../services/zoo-code-auth")
1160+
const upsertProviderProfile = vi.fn().mockResolvedValue(undefined)
1161+
const saveConfig = vi.fn().mockResolvedValue(undefined)
1162+
1163+
;(mockClineProvider as any).contextProxy = {
1164+
...mockClineProvider.contextProxy,
1165+
getProviderSettings: vi.fn().mockReturnValue({ apiProvider: "zoo-gateway" }),
1166+
getValues: vi.fn().mockReturnValue({ currentApiConfigName: "Zoo Gateway" }),
1167+
}
1168+
;(mockClineProvider as any).providerSettingsManager = {
1169+
listConfig: vi.fn().mockResolvedValue([
1170+
{ name: "Zoo Gateway", apiProvider: "zoo-gateway" },
1171+
{ name: "Backup Zoo", apiProvider: "zoo-gateway" },
1172+
]),
1173+
getProfile: vi
1174+
.fn()
1175+
.mockResolvedValueOnce({
1176+
apiProvider: "zoo-gateway",
1177+
zooSessionToken: "token-active",
1178+
zooGatewayModelId: "anthropic/claude-sonnet-4",
1179+
})
1180+
.mockResolvedValueOnce({
1181+
apiProvider: "zoo-gateway",
1182+
zooSessionToken: "token-backup",
1183+
}),
1184+
saveConfig,
1185+
}
1186+
;(mockClineProvider as any).upsertProviderProfile = upsertProviderProfile
1187+
1188+
await webviewMessageHandler(mockClineProvider, { type: "zooCodeSignOut" })
1189+
1190+
expect(disconnectZooCode).toHaveBeenCalled()
1191+
expect(upsertProviderProfile).toHaveBeenCalledWith(
1192+
"Zoo Gateway",
1193+
expect.not.objectContaining({ zooSessionToken: expect.anything() }),
1194+
true,
1195+
)
1196+
expect(saveConfig).toHaveBeenCalledWith(
1197+
"Backup Zoo",
1198+
expect.not.objectContaining({ zooSessionToken: expect.anything() }),
1199+
)
1200+
expect(mockClineProvider.postStateToWebview).toHaveBeenCalled()
1201+
})
1202+
1203+
it("still clears the in-memory handler when the active profile token is already empty on disk", async () => {
1204+
const upsertProviderProfile = vi.fn().mockResolvedValue(undefined)
1205+
1206+
;(mockClineProvider as any).contextProxy = {
1207+
...mockClineProvider.contextProxy,
1208+
getProviderSettings: vi.fn().mockReturnValue({ apiProvider: "zoo-gateway" }),
1209+
getValues: vi.fn().mockReturnValue({ currentApiConfigName: "Zoo Gateway" }),
1210+
}
1211+
;(mockClineProvider as any).providerSettingsManager = {
1212+
listConfig: vi.fn().mockResolvedValue([{ name: "Zoo Gateway", apiProvider: "zoo-gateway" }]),
1213+
getProfile: vi.fn().mockResolvedValue({
1214+
apiProvider: "zoo-gateway",
1215+
zooGatewayModelId: "anthropic/claude-sonnet-4",
1216+
}),
1217+
saveConfig: vi.fn(),
1218+
}
1219+
;(mockClineProvider as any).upsertProviderProfile = upsertProviderProfile
1220+
1221+
await webviewMessageHandler(mockClineProvider, { type: "zooCodeSignOut" })
1222+
1223+
expect(upsertProviderProfile).toHaveBeenCalledWith(
1224+
"Zoo Gateway",
1225+
expect.not.objectContaining({ zooSessionToken: expect.anything() }),
1226+
true,
1227+
)
1228+
})
1229+
})

0 commit comments

Comments
 (0)