mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-12 15:10:59 -07:00
637 lines
23 KiB
TypeScript
637 lines
23 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import {
|
|
classifyGoogleError,
|
|
RetryableQuotaError,
|
|
TerminalQuotaError,
|
|
ValidationRequiredError,
|
|
} from './googleQuotaErrors.js';
|
|
import * as errorParser from './googleErrors.js';
|
|
import type { GoogleApiError } from './googleErrors.js';
|
|
import { ModelNotFoundError } from './httpErrors.js';
|
|
|
|
describe('classifyGoogleError', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should return original error if not a Google API error', () => {
|
|
const regularError = new Error('Something went wrong');
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
|
|
const result = classifyGoogleError(regularError);
|
|
expect(result).toBe(regularError);
|
|
});
|
|
|
|
it('should return RetryableQuotaError when message contains "Please retry in Xs"', () => {
|
|
const complexError = {
|
|
error: {
|
|
message:
|
|
'{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s."}]}}',
|
|
code: 429,
|
|
status: 'Too Many Requests',
|
|
},
|
|
};
|
|
const rawError = new Error(JSON.stringify(complexError));
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
|
|
|
|
const result = classifyGoogleError(rawError);
|
|
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
expect((result as RetryableQuotaError).retryDelayMs).toBe(44097.740004);
|
|
expect((result as RetryableQuotaError).message).toBe(rawError.message);
|
|
});
|
|
|
|
it('should return RetryableQuotaError when error is a string and message contains "Please retry in Xms"', () => {
|
|
const complexErrorString = JSON.stringify({
|
|
error: {
|
|
message:
|
|
'{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms."}]}}',
|
|
code: 429,
|
|
status: 'Too Many Requests',
|
|
},
|
|
});
|
|
const rawError = new Error(complexErrorString);
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
|
|
|
|
const result = classifyGoogleError(rawError);
|
|
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
expect((result as RetryableQuotaError).retryDelayMs).toBeCloseTo(900.2);
|
|
expect((result as RetryableQuotaError).message).toBe(rawError.message);
|
|
});
|
|
|
|
it('should return RetryableQuotaError for 503 Service Unavailable', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 503,
|
|
message: 'Service Unavailable',
|
|
details: [],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const originalError = new Error('Service Unavailable');
|
|
const result = classifyGoogleError(originalError);
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
if (result instanceof RetryableQuotaError) {
|
|
expect(result.cause).toBe(apiError);
|
|
expect(result.message).toBe('Service Unavailable');
|
|
}
|
|
});
|
|
|
|
it('should return original error if code is not 429 or 503', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 500,
|
|
message: 'Server error',
|
|
details: [],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const originalError = new Error();
|
|
const result = classifyGoogleError(originalError);
|
|
expect(result).toBe(originalError);
|
|
expect(result).not.toBeInstanceOf(TerminalQuotaError);
|
|
expect(result).not.toBeInstanceOf(RetryableQuotaError);
|
|
});
|
|
|
|
it('should return TerminalQuotaError for daily quota violations in QuotaFailure', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message: 'Quota exceeded',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.QuotaFailure',
|
|
violations: [
|
|
{
|
|
subject: 'user',
|
|
description: 'daily limit',
|
|
quotaId: 'RequestsPerDay-limit',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(TerminalQuotaError);
|
|
expect((result as TerminalQuotaError).cause).toBe(apiError);
|
|
});
|
|
|
|
it('should return RetryableQuotaError for long retry delays', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message: 'Too many requests',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.RetryInfo',
|
|
retryDelay: '301s', // Any delay is now retryable
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
expect((result as RetryableQuotaError).retryDelayMs).toBe(301000);
|
|
});
|
|
|
|
it('should return RetryableQuotaError for short retry delays', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message: 'Too many requests',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.RetryInfo',
|
|
retryDelay: '45.123s',
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
expect((result as RetryableQuotaError).retryDelayMs).toBe(45123);
|
|
});
|
|
|
|
it('should return RetryableQuotaError for per-minute quota violations in QuotaFailure', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message: 'Quota exceeded',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.QuotaFailure',
|
|
violations: [
|
|
{
|
|
subject: 'user',
|
|
description: 'per minute limit',
|
|
quotaId: 'RequestsPerMinute-limit',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
expect((result as RetryableQuotaError).retryDelayMs).toBe(60000);
|
|
});
|
|
|
|
it('should return RetryableQuotaError for per-minute quota violations in ErrorInfo', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message: 'Quota exceeded',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'QUOTA_EXCEEDED',
|
|
domain: 'googleapis.com',
|
|
metadata: {
|
|
quota_limit: 'RequestsPerMinute_PerProject_PerUser',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
expect((result as RetryableQuotaError).retryDelayMs).toBe(60000);
|
|
});
|
|
|
|
it('should return RetryableQuotaError for another short retry delay', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message:
|
|
'You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits.\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 56.185908122s.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.QuotaFailure',
|
|
violations: [
|
|
{
|
|
quotaMetric:
|
|
'generativelanguage.googleapis.com/generate_content_free_tier_requests',
|
|
quotaId: 'GenerateRequestsPerMinutePerProjectPerModel-FreeTier',
|
|
quotaDimensions: {
|
|
location: 'global',
|
|
model: 'gemini-2.5-pro',
|
|
},
|
|
quotaValue: '2',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.Help',
|
|
links: [
|
|
{
|
|
description: 'Learn more about Gemini API quotas',
|
|
url: 'https://ai.google.dev/gemini-api/docs/rate-limits',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.RetryInfo',
|
|
retryDelay: '56s',
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
expect((result as RetryableQuotaError).retryDelayMs).toBe(56000);
|
|
});
|
|
|
|
it('should return RetryableQuotaError for Cloud Code RATE_LIMIT_EXCEEDED with retry delay', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message:
|
|
'You have exhausted your capacity on this model. Your quota will reset after 0s.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'RATE_LIMIT_EXCEEDED',
|
|
domain: 'cloudcode-pa.googleapis.com',
|
|
metadata: {
|
|
uiMessage: 'true',
|
|
model: 'gemini-2.5-pro',
|
|
quotaResetDelay: '539.477544ms',
|
|
quotaResetTimeStamp: '2025-10-20T19:14:08Z',
|
|
},
|
|
},
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.RetryInfo',
|
|
retryDelay: '0.539477544s',
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
expect((result as RetryableQuotaError).retryDelayMs).toBeCloseTo(
|
|
539.477544,
|
|
);
|
|
});
|
|
|
|
it('should return TerminalQuotaError for Cloud Code QUOTA_EXHAUSTED', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message:
|
|
'You have exhausted your capacity on this model. Your quota will reset after 0s.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'QUOTA_EXHAUSTED',
|
|
domain: 'cloudcode-pa.googleapis.com',
|
|
metadata: {
|
|
uiMessage: 'true',
|
|
model: 'gemini-2.5-pro',
|
|
quotaResetDelay: '539.477544ms',
|
|
quotaResetTimeStamp: '2025-10-20T19:14:08Z',
|
|
},
|
|
},
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.RetryInfo',
|
|
retryDelay: '0.539477544s',
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(TerminalQuotaError);
|
|
});
|
|
|
|
it('should prioritize daily limit over retry info', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message: 'Quota exceeded',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.QuotaFailure',
|
|
violations: [
|
|
{
|
|
subject: 'user',
|
|
description: 'daily limit',
|
|
quotaId: 'RequestsPerDay-limit',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.RetryInfo',
|
|
retryDelay: '10s',
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(TerminalQuotaError);
|
|
});
|
|
|
|
it('should return RetryableQuotaError for any 429', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 429,
|
|
message: 'Too many requests',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.DebugInfo',
|
|
detail: 'some debug info',
|
|
stackEntries: [],
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const originalError = new Error();
|
|
const result = classifyGoogleError(originalError);
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
if (result instanceof RetryableQuotaError) {
|
|
expect(result.retryDelayMs).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('should classify nested JSON string 404 error as ModelNotFoundError', () => {
|
|
// Mimic the double-wrapped JSON structure seen in the user report
|
|
const innerError = {
|
|
error: {
|
|
code: 404,
|
|
message:
|
|
'models/NOT_FOUND is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.',
|
|
status: 'NOT_FOUND',
|
|
},
|
|
};
|
|
const errorString = JSON.stringify(innerError);
|
|
|
|
const outerErrorString = JSON.stringify({
|
|
error: {
|
|
message: errorString,
|
|
},
|
|
});
|
|
const error = new Error(`[API Error: ${outerErrorString}]`);
|
|
|
|
const classified = classifyGoogleError(error);
|
|
expect(classified).toBeInstanceOf(ModelNotFoundError);
|
|
expect((classified as ModelNotFoundError).code).toBe(404);
|
|
});
|
|
|
|
it('should fallback to string parsing for retry delays when details array is empty', () => {
|
|
const errorWithEmptyDetails = {
|
|
error: {
|
|
code: 429,
|
|
message: 'Resource exhausted. Please retry in 5s',
|
|
details: [],
|
|
},
|
|
};
|
|
|
|
const result = classifyGoogleError(errorWithEmptyDetails);
|
|
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
if (result instanceof RetryableQuotaError) {
|
|
expect(result.retryDelayMs).toBe(5000);
|
|
// The cause should be the parsed GoogleApiError
|
|
expect(result.cause).toEqual({
|
|
code: 429,
|
|
message: 'Resource exhausted. Please retry in 5s',
|
|
details: [],
|
|
});
|
|
}
|
|
});
|
|
|
|
it('should return RetryableQuotaError without delay time for generic 429 without specific message', () => {
|
|
const generic429 = {
|
|
status: 429,
|
|
message: 'Resource exhausted. No specific retry info.',
|
|
};
|
|
|
|
const result = classifyGoogleError(generic429);
|
|
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
if (result instanceof RetryableQuotaError) {
|
|
expect(result.retryDelayMs).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('should return RetryableQuotaError without delay time for 429 with empty details and no regex match', () => {
|
|
const errorWithEmptyDetails = {
|
|
error: {
|
|
code: 429,
|
|
message: 'A generic 429 error with no retry message.',
|
|
details: [],
|
|
},
|
|
};
|
|
|
|
const result = classifyGoogleError(errorWithEmptyDetails);
|
|
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
if (result instanceof RetryableQuotaError) {
|
|
expect(result.retryDelayMs).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('should return RetryableQuotaError without delay time for 429 with some detail', () => {
|
|
const errorWithEmptyDetails = {
|
|
error: {
|
|
code: 429,
|
|
message: 'A generic 429 error with no retry message.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'QUOTA_EXCEEDED',
|
|
domain: 'googleapis.com',
|
|
metadata: {
|
|
quota_limit: '',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const result = classifyGoogleError(errorWithEmptyDetails);
|
|
|
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
|
if (result instanceof RetryableQuotaError) {
|
|
expect(result.retryDelayMs).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('should return ValidationRequiredError for 403 with VALIDATION_REQUIRED from cloudcode-pa domain', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 403,
|
|
message: 'Validation required to continue.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'VALIDATION_REQUIRED',
|
|
domain: 'cloudcode-pa.googleapis.com',
|
|
metadata: {
|
|
validation_link: 'https://fallback.example.com/validate',
|
|
},
|
|
},
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.Help',
|
|
links: [
|
|
{
|
|
description: 'Complete validation to continue',
|
|
url: 'https://example.com/validate',
|
|
},
|
|
{
|
|
description: 'Learn more',
|
|
url: 'https://support.google.com/accounts?p=al_alert',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(ValidationRequiredError);
|
|
expect((result as ValidationRequiredError).validationLink).toBe(
|
|
'https://example.com/validate',
|
|
);
|
|
expect((result as ValidationRequiredError).validationDescription).toBe(
|
|
'Complete validation to continue',
|
|
);
|
|
expect((result as ValidationRequiredError).learnMoreUrl).toBe(
|
|
'https://support.google.com/accounts?p=al_alert',
|
|
);
|
|
expect((result as ValidationRequiredError).cause).toBe(apiError);
|
|
});
|
|
|
|
it('should correctly parse Learn more URL when first link description contains "Learn more" text', () => {
|
|
// This tests the real API response format where the description of the first
|
|
// link contains "Learn more:" text, but we should use the second link's URL
|
|
const apiError: GoogleApiError = {
|
|
code: 403,
|
|
message: 'Validation required to continue.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'VALIDATION_REQUIRED',
|
|
domain: 'cloudcode-pa.googleapis.com',
|
|
metadata: {},
|
|
},
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.Help',
|
|
links: [
|
|
{
|
|
description:
|
|
'Further action is required to use this service. Navigate to the following URL to complete verification:\n\nhttps://accounts.sandbox.google.com/signin/continue?...\n\nLearn more:\n\nhttps://support.google.com/accounts?p=al_alert\n',
|
|
url: 'https://accounts.sandbox.google.com/signin/continue?sarp=1&scc=1&continue=...',
|
|
},
|
|
{
|
|
description: 'Learn more',
|
|
url: 'https://support.google.com/accounts?p=al_alert',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(ValidationRequiredError);
|
|
// Should get the validation link from the first link
|
|
expect((result as ValidationRequiredError).validationLink).toBe(
|
|
'https://accounts.sandbox.google.com/signin/continue?sarp=1&scc=1&continue=...',
|
|
);
|
|
// Should get the Learn more URL from the SECOND link, not the first
|
|
expect((result as ValidationRequiredError).learnMoreUrl).toBe(
|
|
'https://support.google.com/accounts?p=al_alert',
|
|
);
|
|
});
|
|
|
|
it('should fallback to ErrorInfo metadata when Help detail is not present', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 403,
|
|
message: 'Validation required.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'VALIDATION_REQUIRED',
|
|
domain: 'staging-cloudcode-pa.googleapis.com',
|
|
metadata: {
|
|
validation_link: 'https://staging.example.com/validate',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(ValidationRequiredError);
|
|
expect((result as ValidationRequiredError).validationLink).toBe(
|
|
'https://staging.example.com/validate',
|
|
);
|
|
expect(
|
|
(result as ValidationRequiredError).validationDescription,
|
|
).toBeUndefined();
|
|
expect((result as ValidationRequiredError).learnMoreUrl).toBeUndefined();
|
|
});
|
|
|
|
it('should return original error for 403 with different reason', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 403,
|
|
message: 'Access denied.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'ACCESS_DENIED',
|
|
domain: 'cloudcode-pa.googleapis.com',
|
|
metadata: {},
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const originalError = new Error();
|
|
const result = classifyGoogleError(originalError);
|
|
expect(result).toBe(originalError);
|
|
expect(result).not.toBeInstanceOf(ValidationRequiredError);
|
|
});
|
|
|
|
it('should find learn more link by hostname when description is different', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 403,
|
|
message: 'Validation required.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'VALIDATION_REQUIRED',
|
|
domain: 'cloudcode-pa.googleapis.com',
|
|
metadata: {},
|
|
},
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.Help',
|
|
links: [
|
|
{
|
|
description: 'Complete validation',
|
|
url: 'https://accounts.google.com/validate',
|
|
},
|
|
{
|
|
description: 'More information', // Not exactly "Learn more"
|
|
url: 'https://support.google.com/accounts?p=al_alert',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const result = classifyGoogleError(new Error());
|
|
expect(result).toBeInstanceOf(ValidationRequiredError);
|
|
expect((result as ValidationRequiredError).learnMoreUrl).toBe(
|
|
'https://support.google.com/accounts?p=al_alert',
|
|
);
|
|
});
|
|
|
|
it('should return original error for 403 from non-cloudcode domain', () => {
|
|
const apiError: GoogleApiError = {
|
|
code: 403,
|
|
message: 'Forbidden.',
|
|
details: [
|
|
{
|
|
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
reason: 'VALIDATION_REQUIRED',
|
|
domain: 'other.googleapis.com',
|
|
metadata: {},
|
|
},
|
|
],
|
|
};
|
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
|
const originalError = new Error();
|
|
const result = classifyGoogleError(originalError);
|
|
expect(result).toBe(originalError);
|
|
expect(result).not.toBeInstanceOf(ValidationRequiredError);
|
|
});
|
|
});
|