test: raise the timeout of the OAuth client suite
The suite re-imports GoogleAPIService after vi.resetModules() in every test; under the parallel full run the first import exceeded 5 s and failed (passes alone in about 1 s).
This commit is contained in:
1 parent
8e20c32117
commit
0a6a8ff4bb
1 file changed
+439
-437
@@ -1,437 +1,439 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { isString } from 'lodash-es';
|
import { isString } from 'lodash-es';
|
||||||
|
|
||||||
vi.mock('@/server/modules/proxy/request-user-agent', () => ({
|
vi.mock('@/server/modules/proxy/request-user-agent', () => ({
|
||||||
resolveRequestUserAgent: vi.fn().mockResolvedValue('test-agent/1.0.0'),
|
resolveRequestUserAgent: vi.fn().mockResolvedValue('test-agent/1.0.0'),
|
||||||
buildUserAgent: vi.fn().mockReturnValue('test-agent/1.0.0'),
|
buildUserAgent: vi.fn().mockReturnValue('test-agent/1.0.0'),
|
||||||
resolveLocalInstalledVersion: vi.fn().mockReturnValue('1.0.0'),
|
resolveLocalInstalledVersion: vi.fn().mockReturnValue('1.0.0'),
|
||||||
FALLBACK_VERSION: '1.22.2',
|
FALLBACK_VERSION: '1.22.2',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock module to test timeout behavior
|
// Mock module to test timeout behavior
|
||||||
describe('GoogleAPIService Timeout', () => {
|
describe('GoogleAPIService Timeout', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should abort request after timeout', async () => {
|
it('should abort request after timeout', async () => {
|
||||||
const TIMEOUT_MS = 100;
|
const TIMEOUT_MS = 100;
|
||||||
|
|
||||||
// Recreate the same logic used in GoogleAPIService
|
// Recreate the same logic used in GoogleAPIService
|
||||||
const createTimeoutSignal = (ms: number): AbortSignal => {
|
const createTimeoutSignal = (ms: number): AbortSignal => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
setTimeout(() => controller.abort(), ms);
|
setTimeout(() => controller.abort(), ms);
|
||||||
return controller.signal;
|
return controller.signal;
|
||||||
};
|
};
|
||||||
|
|
||||||
const signal = createTimeoutSignal(TIMEOUT_MS);
|
const signal = createTimeoutSignal(TIMEOUT_MS);
|
||||||
|
|
||||||
// Verify signal is not aborted initially
|
// Verify signal is not aborted initially
|
||||||
expect(signal.aborted).toBe(false);
|
expect(signal.aborted).toBe(false);
|
||||||
|
|
||||||
// Fast-forward time
|
// Fast-forward time
|
||||||
vi.advanceTimersByTime(TIMEOUT_MS + 10);
|
vi.advanceTimersByTime(TIMEOUT_MS + 10);
|
||||||
|
|
||||||
// Signal should now be aborted
|
// Signal should now be aborted
|
||||||
expect(signal.aborted).toBe(true);
|
expect(signal.aborted).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set aborted state when controller.abort() is called', () => {
|
it('should set aborted state when controller.abort() is called', () => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
// Initially not aborted
|
// Initially not aborted
|
||||||
expect(controller.signal.aborted).toBe(false);
|
expect(controller.signal.aborted).toBe(false);
|
||||||
|
|
||||||
// Abort the controller
|
// Abort the controller
|
||||||
controller.abort();
|
controller.abort();
|
||||||
|
|
||||||
// Signal should now be aborted
|
// Signal should now be aborted
|
||||||
expect(controller.signal.aborted).toBe(true);
|
expect(controller.signal.aborted).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transform AbortError to user-friendly message', () => {
|
it('should transform AbortError to user-friendly message', () => {
|
||||||
// This tests our error transformation logic from GoogleAPIService
|
// This tests our error transformation logic from GoogleAPIService
|
||||||
const handleAbortError = (err: Error) => {
|
const handleAbortError = (err: Error) => {
|
||||||
if (err.name === 'AbortError') {
|
if (err.name === 'AbortError') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Token exchange timed out. Please check your network connection and try again.',
|
'Token exchange timed out. Please check your network connection and try again.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
};
|
};
|
||||||
|
|
||||||
const abortError = new Error('The operation was aborted');
|
const abortError = new Error('The operation was aborted');
|
||||||
abortError.name = 'AbortError';
|
abortError.name = 'AbortError';
|
||||||
|
|
||||||
expect(() => handleAbortError(abortError)).toThrow(
|
expect(() => handleAbortError(abortError)).toThrow(
|
||||||
'Token exchange timed out. Please check your network connection and try again.',
|
'Token exchange timed out. Please check your network connection and try again.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not transform non-AbortError', () => {
|
it('should not transform non-AbortError', () => {
|
||||||
const handleAbortError = (err: Error) => {
|
const handleAbortError = (err: Error) => {
|
||||||
if (err.name === 'AbortError') {
|
if (err.name === 'AbortError') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Token exchange timed out. Please check your network connection and try again.',
|
'Token exchange timed out. Please check your network connection and try again.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
};
|
};
|
||||||
|
|
||||||
const networkError = new Error('Network failure');
|
const networkError = new Error('Network failure');
|
||||||
networkError.name = 'NetworkError';
|
networkError.name = 'NetworkError';
|
||||||
|
|
||||||
expect(() => handleAbortError(networkError)).toThrow('Network failure');
|
expect(() => handleAbortError(networkError)).toThrow('Network failure');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GoogleAPIService OAuth clients', () => {
|
// Each test resets the module registry and re-imports GoogleAPIService, which can exceed the default
|
||||||
const originalOauthClientsEnv = process.env.ANTIGRAVITY_OAUTH_CLIENTS;
|
// 5s timeout while the whole suite runs in parallel.
|
||||||
const originalActiveOauthClientEnv = process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY;
|
describe('GoogleAPIService OAuth clients', { timeout: 30_000 }, () => {
|
||||||
|
const originalOauthClientsEnv = process.env.ANTIGRAVITY_OAUTH_CLIENTS;
|
||||||
beforeEach(() => {
|
const originalActiveOauthClientEnv = process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY;
|
||||||
vi.resetModules();
|
|
||||||
delete process.env.ANTIGRAVITY_OAUTH_CLIENTS;
|
beforeEach(() => {
|
||||||
delete process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY;
|
vi.resetModules();
|
||||||
});
|
delete process.env.ANTIGRAVITY_OAUTH_CLIENTS;
|
||||||
|
delete process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY;
|
||||||
afterEach(() => {
|
});
|
||||||
if (isString(originalOauthClientsEnv)) {
|
|
||||||
process.env.ANTIGRAVITY_OAUTH_CLIENTS = originalOauthClientsEnv;
|
afterEach(() => {
|
||||||
} else {
|
if (isString(originalOauthClientsEnv)) {
|
||||||
delete process.env.ANTIGRAVITY_OAUTH_CLIENTS;
|
process.env.ANTIGRAVITY_OAUTH_CLIENTS = originalOauthClientsEnv;
|
||||||
}
|
} else {
|
||||||
|
delete process.env.ANTIGRAVITY_OAUTH_CLIENTS;
|
||||||
if (isString(originalActiveOauthClientEnv)) {
|
}
|
||||||
process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY = originalActiveOauthClientEnv;
|
|
||||||
} else {
|
if (isString(originalActiveOauthClientEnv)) {
|
||||||
delete process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY;
|
process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY = originalActiveOauthClientEnv;
|
||||||
}
|
} else {
|
||||||
});
|
delete process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY;
|
||||||
|
}
|
||||||
it('loads builtin and custom oauth clients with active marker', async () => {
|
});
|
||||||
process.env.ANTIGRAVITY_OAUTH_CLIENTS =
|
|
||||||
'custom_a|id-a|secret-a|Custom A;custom_b|id-b|secret-b|Custom B';
|
it('loads builtin and custom oauth clients with active marker', async () => {
|
||||||
process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY = 'custom_b';
|
process.env.ANTIGRAVITY_OAUTH_CLIENTS =
|
||||||
|
'custom_a|id-a|secret-a|Custom A;custom_b|id-b|secret-b|Custom B';
|
||||||
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY = 'custom_b';
|
||||||
const clients = GoogleAPIService.listOAuthClients();
|
|
||||||
|
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
||||||
expect(clients.some((client) => client.is_builtin)).toBe(true);
|
const clients = GoogleAPIService.listOAuthClients();
|
||||||
expect(clients.find((client) => client.key === 'custom_a')?.label).toBe('Custom A');
|
|
||||||
expect(clients.find((client) => client.key === 'custom_b')?.is_active).toBe(true);
|
expect(clients.some((client) => client.is_builtin)).toBe(true);
|
||||||
});
|
expect(clients.find((client) => client.key === 'custom_a')?.label).toBe('Custom A');
|
||||||
|
expect(clients.find((client) => client.key === 'custom_b')?.is_active).toBe(true);
|
||||||
it('switches active oauth client key', async () => {
|
});
|
||||||
process.env.ANTIGRAVITY_OAUTH_CLIENTS = 'custom_a|id-a|secret-a|Custom A';
|
|
||||||
|
it('switches active oauth client key', async () => {
|
||||||
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
process.env.ANTIGRAVITY_OAUTH_CLIENTS = 'custom_a|id-a|secret-a|Custom A';
|
||||||
GoogleAPIService.setActiveOAuthClientKey('custom_a');
|
|
||||||
|
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
||||||
expect(GoogleAPIService.getActiveOAuthClientKey()).toBe('custom_a');
|
GoogleAPIService.setActiveOAuthClientKey('custom_a');
|
||||||
});
|
|
||||||
|
expect(GoogleAPIService.getActiveOAuthClientKey()).toBe('custom_a');
|
||||||
it('throws when switching to unknown oauth client key', async () => {
|
});
|
||||||
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
|
||||||
expect(() => GoogleAPIService.setActiveOAuthClientKey('missing_client')).toThrow(
|
it('throws when switching to unknown oauth client key', async () => {
|
||||||
'Unknown OAuth client key',
|
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
||||||
);
|
expect(() => GoogleAPIService.setActiveOAuthClientKey('missing_client')).toThrow(
|
||||||
});
|
'Unknown OAuth client key',
|
||||||
});
|
);
|
||||||
|
});
|
||||||
describe('GoogleAPIService user info parsing', () => {
|
});
|
||||||
beforeEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
describe('GoogleAPIService user info parsing', () => {
|
||||||
vi.resetModules();
|
beforeEach(() => {
|
||||||
});
|
vi.restoreAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
it('accepts Google user info responses without family_name', async () => {
|
});
|
||||||
const fetchMock = vi.fn().mockResolvedValueOnce({
|
|
||||||
ok: true,
|
it('accepts Google user info responses without family_name', async () => {
|
||||||
json: vi.fn().mockResolvedValue({
|
const fetchMock = vi.fn().mockResolvedValueOnce({
|
||||||
id: 'google-user-1',
|
ok: true,
|
||||||
email: '[email protected]',
|
json: vi.fn().mockResolvedValue({
|
||||||
verified_email: true,
|
id: 'google-user-1',
|
||||||
name: 'Example User',
|
email: 'user@example.com',
|
||||||
given_name: 'Example',
|
verified_email: true,
|
||||||
picture: 'https://example.com/avatar.png',
|
name: 'Example User',
|
||||||
}),
|
given_name: 'Example',
|
||||||
});
|
picture: 'https://example.com/avatar.png',
|
||||||
|
}),
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
});
|
||||||
|
|
||||||
const { ConfigManager } = await import('../../ipc/config/manager');
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
|
||||||
proxy: {
|
const { ConfigManager } = await import('../../ipc/config/manager');
|
||||||
upstream_proxy: {
|
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
||||||
enabled: false,
|
proxy: {
|
||||||
},
|
upstream_proxy: {
|
||||||
},
|
enabled: false,
|
||||||
} as any);
|
},
|
||||||
|
},
|
||||||
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
} as any);
|
||||||
|
|
||||||
await expect(GoogleAPIService.getUserInfo('access-token')).resolves.toEqual(
|
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
||||||
expect.objectContaining({
|
|
||||||
id: 'google-user-1',
|
await expect(GoogleAPIService.getUserInfo('access-token')).resolves.toEqual(
|
||||||
email: '[email protected]',
|
expect.objectContaining({
|
||||||
name: 'Example User',
|
id: 'google-user-1',
|
||||||
given_name: 'Example',
|
email: 'user@example.com',
|
||||||
picture: 'https://example.com/avatar.png',
|
name: 'Example User',
|
||||||
verified_email: true,
|
given_name: 'Example',
|
||||||
}),
|
picture: 'https://example.com/avatar.png',
|
||||||
);
|
verified_email: true,
|
||||||
});
|
}),
|
||||||
});
|
);
|
||||||
|
});
|
||||||
describe('CloudAccountList auth code auto-submit guard', () => {
|
});
|
||||||
it('returns true for a fresh auth code while the dialog is open', async () => {
|
|
||||||
const { shouldAutoSubmitGoogleAuthCode } = await import('../../utils/googleAuthSubmission');
|
describe('CloudAccountList auth code auto-submit guard', () => {
|
||||||
|
it('returns true for a fresh auth code while the dialog is open', async () => {
|
||||||
expect(
|
const { shouldAutoSubmitGoogleAuthCode } = await import('../../utils/googleAuthSubmission');
|
||||||
shouldAutoSubmitGoogleAuthCode({
|
|
||||||
authCode: 'fresh-code',
|
expect(
|
||||||
isAddDialogOpen: true,
|
shouldAutoSubmitGoogleAuthCode({
|
||||||
isPending: false,
|
authCode: 'fresh-code',
|
||||||
lastSubmittedAuthCode: null,
|
isAddDialogOpen: true,
|
||||||
}),
|
isPending: false,
|
||||||
).toBe(true);
|
lastSubmittedAuthCode: null,
|
||||||
});
|
}),
|
||||||
|
).toBe(true);
|
||||||
it('returns false after the same auth code was already auto-submitted', async () => {
|
});
|
||||||
const { shouldAutoSubmitGoogleAuthCode } = await import('../../utils/googleAuthSubmission');
|
|
||||||
|
it('returns false after the same auth code was already auto-submitted', async () => {
|
||||||
expect(
|
const { shouldAutoSubmitGoogleAuthCode } = await import('../../utils/googleAuthSubmission');
|
||||||
shouldAutoSubmitGoogleAuthCode({
|
|
||||||
authCode: 'same-code',
|
expect(
|
||||||
isAddDialogOpen: true,
|
shouldAutoSubmitGoogleAuthCode({
|
||||||
isPending: false,
|
authCode: 'same-code',
|
||||||
lastSubmittedAuthCode: 'same-code',
|
isAddDialogOpen: true,
|
||||||
}),
|
isPending: false,
|
||||||
).toBe(false);
|
lastSubmittedAuthCode: 'same-code',
|
||||||
});
|
}),
|
||||||
});
|
).toBe(false);
|
||||||
|
});
|
||||||
describe('GoogleAPIService fetchQuota fallback policy', () => {
|
});
|
||||||
beforeEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
describe('GoogleAPIService fetchQuota fallback policy', () => {
|
||||||
vi.resetModules();
|
beforeEach(() => {
|
||||||
});
|
vi.restoreAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
it('falls back to fetchCredits when loadCodeAssist returns no usable credits', async () => {
|
});
|
||||||
const fetchMock = vi
|
|
||||||
.fn()
|
it('falls back to fetchCredits when loadCodeAssist returns no usable credits', async () => {
|
||||||
.mockResolvedValueOnce({
|
const fetchMock = vi
|
||||||
ok: true,
|
.fn()
|
||||||
status: 200,
|
.mockResolvedValueOnce({
|
||||||
json: vi.fn().mockResolvedValue({
|
ok: true,
|
||||||
// No paidTier.availableCredits — no usable credits
|
status: 200,
|
||||||
freeTier: {},
|
json: vi.fn().mockResolvedValue({
|
||||||
}),
|
// No paidTier.availableCredits — no usable credits
|
||||||
})
|
freeTier: {},
|
||||||
.mockResolvedValueOnce({
|
}),
|
||||||
ok: true,
|
})
|
||||||
status: 200,
|
.mockResolvedValueOnce({
|
||||||
json: vi.fn().mockResolvedValue({
|
ok: true,
|
||||||
credits: 500,
|
status: 200,
|
||||||
expiryDate: '2025-12-31',
|
json: vi.fn().mockResolvedValue({
|
||||||
}),
|
credits: 500,
|
||||||
});
|
expiryDate: '2025-12-31',
|
||||||
|
}),
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
});
|
||||||
|
|
||||||
const { ConfigManager } = await import('../../ipc/config/manager');
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
|
||||||
proxy: {
|
const { ConfigManager } = await import('../../ipc/config/manager');
|
||||||
upstream_proxy: {
|
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
||||||
enabled: false,
|
proxy: {
|
||||||
},
|
upstream_proxy: {
|
||||||
},
|
enabled: false,
|
||||||
} as any);
|
},
|
||||||
|
},
|
||||||
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
} as any);
|
||||||
|
|
||||||
await expect(GoogleAPIService.fetchAICredits('access-token')).resolves.toEqual({
|
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
||||||
credits: 500,
|
|
||||||
expiryDate: '2025-12-31',
|
await expect(GoogleAPIService.fetchAICredits('access-token')).resolves.toEqual({
|
||||||
});
|
credits: 500,
|
||||||
|
expiryDate: '2025-12-31',
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
});
|
||||||
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
||||||
'https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist',
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||||
);
|
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
||||||
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
'https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist',
|
||||||
'https://cloudcode-pa.googleapis.com/v1internal:fetchCredits',
|
);
|
||||||
);
|
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
||||||
});
|
'https://cloudcode-pa.googleapis.com/v1internal:fetchCredits',
|
||||||
|
);
|
||||||
it('does not fall through to the next endpoint on permanent 400 errors', async () => {
|
});
|
||||||
const fetchMock = vi.fn().mockResolvedValueOnce({
|
|
||||||
ok: false,
|
it('does not fall through to the next endpoint on permanent 400 errors', async () => {
|
||||||
status: 400,
|
const fetchMock = vi.fn().mockResolvedValueOnce({
|
||||||
text: vi.fn().mockResolvedValue('INVALID_ARGUMENT'),
|
ok: false,
|
||||||
});
|
status: 400,
|
||||||
|
text: vi.fn().mockResolvedValue('INVALID_ARGUMENT'),
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
});
|
||||||
|
|
||||||
const { ConfigManager } = await import('../../ipc/config/manager');
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
|
||||||
proxy: {
|
const { ConfigManager } = await import('../../ipc/config/manager');
|
||||||
upstream_proxy: {
|
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
||||||
enabled: false,
|
proxy: {
|
||||||
},
|
upstream_proxy: {
|
||||||
},
|
enabled: false,
|
||||||
} as any);
|
},
|
||||||
|
},
|
||||||
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
} as any);
|
||||||
vi.spyOn(GoogleAPIService, 'fetchProjectContext').mockResolvedValue({
|
|
||||||
projectId: 'project-1',
|
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
||||||
subscriptionTier: 'free',
|
vi.spyOn(GoogleAPIService, 'fetchProjectContext').mockResolvedValue({
|
||||||
});
|
projectId: 'project-1',
|
||||||
|
subscriptionTier: 'free',
|
||||||
await expect(GoogleAPIService.fetchQuota('access-token')).rejects.toThrow(
|
});
|
||||||
'HTTP 400 - INVALID_ARGUMENT',
|
|
||||||
);
|
await expect(GoogleAPIService.fetchQuota('access-token')).rejects.toThrow(
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
'HTTP 400 - INVALID_ARGUMENT',
|
||||||
});
|
);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
it('uses an explicit dispatcher from env proxy settings when no account proxy is provided', async () => {
|
});
|
||||||
process.env.HTTP_PROXY = 'http://127.0.0.1:9090';
|
|
||||||
process.env.HTTPS_PROXY = 'http://127.0.0.1:9090';
|
it('uses an explicit dispatcher from env proxy settings when no account proxy is provided', async () => {
|
||||||
process.env.NO_PROXY = 'localhost,127.0.0.1,::1';
|
process.env.HTTP_PROXY = 'http://127.0.0.1:9090';
|
||||||
|
process.env.HTTPS_PROXY = 'http://127.0.0.1:9090';
|
||||||
const fetchMock = vi.fn().mockResolvedValueOnce({
|
process.env.NO_PROXY = 'localhost,127.0.0.1,::1';
|
||||||
ok: true,
|
|
||||||
status: 200,
|
const fetchMock = vi.fn().mockResolvedValueOnce({
|
||||||
json: vi.fn().mockResolvedValue({
|
ok: true,
|
||||||
paidTier: {
|
status: 200,
|
||||||
availableCredits: [
|
json: vi.fn().mockResolvedValue({
|
||||||
{
|
paidTier: {
|
||||||
creditType: 'GOOGLE_ONE_AI',
|
availableCredits: [
|
||||||
creditAmount: '1000',
|
{
|
||||||
minimumCreditAmountForUsage: '50',
|
creditType: 'GOOGLE_ONE_AI',
|
||||||
},
|
creditAmount: '1000',
|
||||||
],
|
minimumCreditAmountForUsage: '50',
|
||||||
},
|
},
|
||||||
}),
|
],
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
});
|
||||||
|
|
||||||
const { ConfigManager } = await import('../../ipc/config/manager');
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
|
||||||
proxy: {
|
const { ConfigManager } = await import('../../ipc/config/manager');
|
||||||
upstream_proxy: {
|
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
||||||
enabled: false,
|
proxy: {
|
||||||
},
|
upstream_proxy: {
|
||||||
},
|
enabled: false,
|
||||||
} as any);
|
},
|
||||||
|
},
|
||||||
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
} as any);
|
||||||
|
|
||||||
await expect(GoogleAPIService.fetchAICredits('access-token')).resolves.toEqual({
|
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
||||||
credits: 1000,
|
|
||||||
expiryDate: '',
|
await expect(GoogleAPIService.fetchAICredits('access-token')).resolves.toEqual({
|
||||||
});
|
credits: 1000,
|
||||||
|
expiryDate: '',
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
});
|
||||||
expect(fetchMock.mock.calls[0]?.[1]?.dispatcher).toBeDefined();
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
delete process.env.HTTP_PROXY;
|
expect(fetchMock.mock.calls[0]?.[1]?.dispatcher).toBeDefined();
|
||||||
delete process.env.HTTPS_PROXY;
|
|
||||||
delete process.env.NO_PROXY;
|
delete process.env.HTTP_PROXY;
|
||||||
});
|
delete process.env.HTTPS_PROXY;
|
||||||
|
delete process.env.NO_PROXY;
|
||||||
it('uses electron proxy env as an explicit dispatcher fallback', async () => {
|
});
|
||||||
process.env.ELECTRON_PROXY_SERVER = 'http://127.0.0.1:9090';
|
|
||||||
|
it('uses electron proxy env as an explicit dispatcher fallback', async () => {
|
||||||
const fetchMock = vi.fn().mockResolvedValueOnce({
|
process.env.ELECTRON_PROXY_SERVER = 'http://127.0.0.1:9090';
|
||||||
ok: true,
|
|
||||||
status: 200,
|
const fetchMock = vi.fn().mockResolvedValueOnce({
|
||||||
json: vi.fn().mockResolvedValue({
|
ok: true,
|
||||||
paidTier: {
|
status: 200,
|
||||||
availableCredits: [
|
json: vi.fn().mockResolvedValue({
|
||||||
{
|
paidTier: {
|
||||||
creditType: 'GOOGLE_ONE_AI',
|
availableCredits: [
|
||||||
creditAmount: '1000',
|
{
|
||||||
minimumCreditAmountForUsage: '50',
|
creditType: 'GOOGLE_ONE_AI',
|
||||||
},
|
creditAmount: '1000',
|
||||||
],
|
minimumCreditAmountForUsage: '50',
|
||||||
},
|
},
|
||||||
}),
|
],
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
});
|
||||||
|
|
||||||
const { ConfigManager } = await import('../../ipc/config/manager');
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
|
||||||
proxy: {
|
const { ConfigManager } = await import('../../ipc/config/manager');
|
||||||
upstream_proxy: {
|
vi.spyOn(ConfigManager, 'loadConfig').mockReturnValue({
|
||||||
enabled: false,
|
proxy: {
|
||||||
},
|
upstream_proxy: {
|
||||||
},
|
enabled: false,
|
||||||
} as any);
|
},
|
||||||
|
},
|
||||||
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
} as any);
|
||||||
|
|
||||||
await expect(GoogleAPIService.fetchAICredits('access-token')).resolves.toEqual({
|
const { GoogleAPIService } = await import('../../services/GoogleAPIService');
|
||||||
credits: 1000,
|
|
||||||
expiryDate: '',
|
await expect(GoogleAPIService.fetchAICredits('access-token')).resolves.toEqual({
|
||||||
});
|
credits: 1000,
|
||||||
|
expiryDate: '',
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
});
|
||||||
expect(fetchMock.mock.calls[0]?.[1]?.dispatcher).toBeDefined();
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
delete process.env.ELECTRON_PROXY_SERVER;
|
expect(fetchMock.mock.calls[0]?.[1]?.dispatcher).toBeDefined();
|
||||||
});
|
|
||||||
});
|
delete process.env.ELECTRON_PROXY_SERVER;
|
||||||
|
});
|
||||||
describe('QuotaService fallback policy', () => {
|
});
|
||||||
beforeEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
describe('QuotaService fallback policy', () => {
|
||||||
vi.resetModules();
|
beforeEach(() => {
|
||||||
});
|
vi.restoreAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
it('does not fall through to the next endpoint on permanent 400 errors', async () => {
|
});
|
||||||
const postMock = vi
|
|
||||||
.fn()
|
it('does not fall through to the next endpoint on permanent 400 errors', async () => {
|
||||||
.mockResolvedValueOnce({
|
const postMock = vi
|
||||||
status: 200,
|
.fn()
|
||||||
data: {
|
.mockResolvedValueOnce({
|
||||||
cloudaicompanionProject: 'project-1',
|
status: 200,
|
||||||
currentTier: { id: 'free' },
|
data: {
|
||||||
},
|
cloudaicompanionProject: 'project-1',
|
||||||
})
|
currentTier: { id: 'free' },
|
||||||
.mockRejectedValueOnce({
|
},
|
||||||
isAxiosError: true,
|
})
|
||||||
response: {
|
.mockRejectedValueOnce({
|
||||||
status: 400,
|
isAxiosError: true,
|
||||||
data: { error: 'INVALID_ARGUMENT' },
|
response: {
|
||||||
},
|
status: 400,
|
||||||
message: 'Request failed with status code 400',
|
data: { error: 'INVALID_ARGUMENT' },
|
||||||
});
|
},
|
||||||
|
message: 'Request failed with status code 400',
|
||||||
vi.doMock('axios', () => ({
|
});
|
||||||
default: {
|
|
||||||
create: vi.fn(() => ({
|
vi.doMock('axios', () => ({
|
||||||
post: postMock,
|
default: {
|
||||||
})),
|
create: vi.fn(() => ({
|
||||||
isAxiosError: (error: unknown) =>
|
post: postMock,
|
||||||
Boolean((error as { isAxiosError?: boolean })?.isAxiosError),
|
})),
|
||||||
},
|
isAxiosError: (error: unknown) =>
|
||||||
isAxiosError: (error: unknown) =>
|
Boolean((error as { isAxiosError?: boolean })?.isAxiosError),
|
||||||
Boolean((error as { isAxiosError?: boolean })?.isAxiosError),
|
},
|
||||||
}));
|
isAxiosError: (error: unknown) =>
|
||||||
|
Boolean((error as { isAxiosError?: boolean })?.isAxiosError),
|
||||||
const { QuotaService } = await import('../../lib/geminiNexus/QuotaService');
|
}));
|
||||||
|
|
||||||
await expect(QuotaService.fetchQuota('access-token', '[email protected]')).rejects.toThrow(
|
const { QuotaService } = await import('../../lib/geminiNexus/QuotaService');
|
||||||
'HTTP 400 - {"error":"INVALID_ARGUMENT"}',
|
|
||||||
);
|
await expect(QuotaService.fetchQuota('access-token', '[email protected]')).rejects.toThrow(
|
||||||
expect(postMock).toHaveBeenCalledTimes(2);
|
'HTTP 400 - {"error":"INVALID_ARGUMENT"}',
|
||||||
});
|
);
|
||||||
});
|
expect(postMock).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user