2025-10-24 11:09:06 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
import { describe , it , expect , vi , afterEach } from 'vitest' ;
import {
classifyGoogleError ,
RetryableQuotaError ,
TerminalQuotaError ,
2026-01-20 16:23:01 -08:00
ValidationRequiredError ,
2025-10-24 11:09:06 -07:00
} from './googleQuotaErrors.js' ;
import * as errorParser from './googleErrors.js' ;
import type { GoogleApiError } from './googleErrors.js' ;
2025-12-10 11:14:07 -08:00
import { ModelNotFoundError } from './httpErrors.js' ;
2025-10-24 11:09:06 -07:00
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 ) ;
} ) ;
2025-11-05 16:42:58 +00:00
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 ) ;
} ) ;
2026-02-20 14:51:53 -05:00
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' , ( ) = > {
2025-10-24 11:09:06 -07:00
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 ) ;
} ) ;
2026-01-22 17:33:32 -08:00
it ( 'should return RetryableQuotaError for long retry delays' , ( ) = > {
2025-10-24 11:09:06 -07:00
const apiError : GoogleApiError = {
code : 429 ,
message : 'Too many requests' ,
details : [
{
'@type' : 'type.googleapis.com/google.rpc.RetryInfo' ,
2026-01-22 17:33:32 -08:00
retryDelay : '301s' , // Any delay is now retryable
2025-10-24 11:09:06 -07:00
} ,
] ,
} ;
vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( apiError ) ;
const result = classifyGoogleError ( new Error ( ) ) ;
2026-01-22 17:33:32 -08:00
expect ( result ) . toBeInstanceOf ( RetryableQuotaError ) ;
expect ( ( result as RetryableQuotaError ) . retryDelayMs ) . toBe ( 301000 ) ;
2025-10-24 11:09:06 -07:00
} ) ;
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 ) ;
} ) ;
2025-12-18 18:25:42 -05:00
it ( 'should return RetryableQuotaError for any 429' , ( ) = > {
2025-10-24 11:09:06 -07:00
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 ) ;
2025-12-18 18:25:42 -05:00
expect ( result ) . toBeInstanceOf ( RetryableQuotaError ) ;
if ( result instanceof RetryableQuotaError ) {
2025-12-29 17:23:23 -05:00
expect ( result . retryDelayMs ) . toBeUndefined ( ) ;
2025-12-18 18:25:42 -05:00
}
2025-10-24 11:09:06 -07:00
} ) ;
2025-12-10 11:14:07 -08:00
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 : [ ] ,
} ) ;
}
} ) ;
2025-12-18 18:25:42 -05:00
2025-12-29 17:23:23 -05:00
it ( 'should return RetryableQuotaError without delay time for generic 429 without specific message' , ( ) = > {
2025-12-18 18:25:42 -05:00
const generic429 = {
status : 429 ,
message : 'Resource exhausted. No specific retry info.' ,
} ;
const result = classifyGoogleError ( generic429 ) ;
expect ( result ) . toBeInstanceOf ( RetryableQuotaError ) ;
if ( result instanceof RetryableQuotaError ) {
2025-12-29 17:23:23 -05:00
expect ( result . retryDelayMs ) . toBeUndefined ( ) ;
2025-12-18 18:25:42 -05:00
}
} ) ;
2025-12-29 17:23:23 -05:00
it ( 'should return RetryableQuotaError without delay time for 429 with empty details and no regex match' , ( ) = > {
2025-12-18 18:25:42 -05:00
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 ) {
2025-12-29 17:23:23 -05:00
expect ( result . retryDelayMs ) . toBeUndefined ( ) ;
2025-12-18 18:25:42 -05:00
}
} ) ;
2025-12-29 17:23:23 -05:00
it ( 'should return RetryableQuotaError without delay time for 429 with some detail' , ( ) = > {
2025-12-18 18:25:42 -05:00
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 ) {
2025-12-29 17:23:23 -05:00
expect ( result . retryDelayMs ) . toBeUndefined ( ) ;
2025-12-18 18:25:42 -05:00
}
} ) ;
2026-01-20 16:23:01 -08:00
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 ) ;
} ) ;
2025-10-24 11:09:06 -07:00
} ) ;