diff --git a/src/lib/integrations/oauth-kit/__tests__/__golden__/oauth-wire.json b/src/lib/integrations/oauth-kit/__tests__/__golden__/oauth-wire.json new file mode 100644 index 000000000..35c077e13 --- /dev/null +++ b/src/lib/integrations/oauth-kit/__tests__/__golden__/oauth-wire.json @@ -0,0 +1,4622 @@ +{ + "withings": { + "redirectUri": "https://healthlog.example/api/withings/callback", + "authorize": { + "url": "https://account.withings.com/oauth2_user/authorize2", + "query": { + "client_id": "client-id-1", + "redirect_uri": "https://healthlog.example/api/withings/callback", + "response_type": "code", + "scope": "user.metrics,user.activity", + "state": "state-nonce-1" + } + }, + "exchange": { + "ok": { + "result": { + "userid": "4711", + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/withings/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "error": { + "name": "SyntaxError", + "message": "Unexpected end of JSON input" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/withings/callback" + }, + "body": null + } + ], + "events": [], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "WithingsApiError", + "message": "Withings token error: 503 - Invalid Params", + "classification": "transient", + "withingsStatus": 503, + "reason": "withings_503", + "verb": "token" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/withings/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200, + "error": "withings_503" + } + ] + } + ], + "unansweredScript": 0 + }, + "revoked": { + "error": { + "name": "WithingsApiError", + "message": "Withings token error: 100 - The hash is missing", + "classification": "reauth_required", + "withingsStatus": 100, + "reason": "withings_100", + "verb": "token" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/withings/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200, + "error": "withings_100" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "WithingsApiError", + "message": "Withings token error: 401 - invalid_client", + "classification": "transient", + "withingsStatus": 401, + "reason": "withings_401_unknown", + "verb": "token" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/withings/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200, + "error": "withings_401_unknown" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "WithingsApiError", + "message": "Withings token error: 601 - Too Many Requests", + "classification": "transient", + "withingsStatus": 601, + "reason": "withings_601", + "verb": "token" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/withings/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200, + "error": "withings_601" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "SyntaxError", + "message": "Unexpected token '<', \"unav\"... is not valid JSON" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/withings/callback" + }, + "body": null + } + ], + "events": [], + "unansweredScript": 0 + } + }, + "refresh": { + "ok": { + "result": { + "userid": "4711", + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "error": { + "name": "SyntaxError", + "message": "Unexpected end of JSON input" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "WithingsApiError", + "message": "Withings refresh error: 503 - Invalid Params", + "classification": "transient", + "withingsStatus": 503, + "reason": "withings_503", + "verb": "refresh" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200, + "error": "withings_503" + } + ] + } + ], + "unansweredScript": 0 + }, + "revoked": { + "error": { + "name": "WithingsApiError", + "message": "Withings refresh error: 100 - The hash is missing", + "classification": "reauth_required", + "withingsStatus": 100, + "reason": "withings_100", + "verb": "refresh" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200, + "error": "withings_100" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "WithingsApiError", + "message": "Withings refresh error: 401 - invalid_client", + "classification": "transient", + "withingsStatus": 401, + "reason": "withings_401_unknown", + "verb": "refresh" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200, + "error": "withings_401_unknown" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "WithingsApiError", + "message": "Withings refresh error: 601 - Too Many Requests", + "classification": "transient", + "withingsStatus": 601, + "reason": "withings_601", + "verb": "refresh" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "withings", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200, + "error": "withings_601" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "SyntaxError", + "message": "Unexpected token '<', \"unav\"... is not valid JSON" + }, + "requests": [ + { + "method": "POST", + "url": "https://wbsapi.withings.net/v2/oauth2", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "action": "requesttoken", + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [], + "unansweredScript": 0 + } + } + }, + "whoop": { + "redirectUri": "https://healthlog.example/api/whoop/callback", + "authorize": { + "url": "https://api.prod.whoop.com/oauth/oauth2/auth", + "query": { + "client_id": "client-id-1", + "redirect_uri": "https://healthlog.example/api/whoop/callback", + "response_type": "code", + "scope": "offline read:recovery read:sleep read:workout read:cycles read:profile read:body_measurement", + "state": "state-nonce-1" + } + }, + "exchange": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/whoop/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "result": null, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/whoop/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP exchangeCode error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "upstreamError": "invalid_grant", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/whoop/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP exchangeCode error: 400", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/whoop/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP exchangeCode error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "exchangeCode", + "upstreamError": "invalid_client", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/whoop/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "exchangeCode", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP exchangeCode error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "exchangeCode", + "upstreamError": "rate_limited", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/whoop/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "exchangeCode", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP exchangeCode error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "exchangeCode", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/whoop/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "exchangeCode", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + } + }, + "refresh": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0", + "scope": "offline" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "result": null, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0", + "scope": "offline" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP refreshAccessToken error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "upstreamError": "invalid_grant", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0", + "scope": "offline" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP refreshAccessToken error: 400", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0", + "scope": "offline" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP refreshAccessToken error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "refreshAccessToken", + "upstreamError": "invalid_client", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0", + "scope": "offline" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP refreshAccessToken error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "refreshAccessToken", + "upstreamError": "rate_limited", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0", + "scope": "offline" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "WhoopApiError", + "message": "WHOOP refreshAccessToken error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "refreshAccessToken", + "vendor": "WHOOP" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.prod.whoop.com/oauth/oauth2/token", + "query": {}, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0", + "scope": "offline" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "whoop", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + } + } + }, + "strava": { + "redirectUri": "https://healthlog.example/api/strava/callback", + "authorize": { + "url": "https://www.strava.com/oauth/authorize", + "query": { + "approval_prompt": "auto", + "client_id": "client-id-1", + "redirect_uri": "https://healthlog.example/api/strava/callback", + "response_type": "code", + "scope": "activity:read_all", + "state": "state-nonce-1" + } + }, + "exchange": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "result": null, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "StravaApiError", + "message": "Strava exchangeCode error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "upstreamError": "invalid_grant", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "StravaApiError", + "message": "Strava exchangeCode error: 400 - RefreshToken.refresh_token:invalid", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "upstreamError": "RefreshToken.refresh_token:invalid", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "StravaApiError", + "message": "Strava exchangeCode error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "exchangeCode", + "upstreamError": "invalid_client", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "exchangeCode", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "StravaApiError", + "message": "Strava exchangeCode error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "exchangeCode", + "upstreamError": "rate_limited", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "exchangeCode", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "StravaApiError", + "message": "Strava exchangeCode error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "exchangeCode", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "exchangeCode", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + } + }, + "refresh": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "result": null, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "StravaApiError", + "message": "Strava refreshAccessToken error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "upstreamError": "invalid_grant", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "StravaApiError", + "message": "Strava refreshAccessToken error: 400 - RefreshToken.refresh_token:invalid", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "upstreamError": "RefreshToken.refresh_token:invalid", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "StravaApiError", + "message": "Strava refreshAccessToken error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "refreshAccessToken", + "upstreamError": "invalid_client", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "StravaApiError", + "message": "Strava refreshAccessToken error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "refreshAccessToken", + "upstreamError": "rate_limited", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "StravaApiError", + "message": "Strava refreshAccessToken error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "refreshAccessToken", + "vendor": "Strava" + }, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + } + }, + "deauthorize_ok": { + "result": null, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/deauthorize", + "query": {}, + "headers": { + "Accept": "application/json", + "Authorization": "Bearer access-1" + }, + "form": null, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "deauthorize", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "deauthorize_refused": { + "result": null, + "requests": [ + { + "method": "POST", + "url": "https://www.strava.com/oauth/deauthorize", + "query": {}, + "headers": { + "Accept": "application/json", + "Authorization": "Bearer access-1" + }, + "form": null, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "strava", + "method": "deauthorize", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + } + }, + "oura": { + "redirectUri": "https://healthlog.example/api/oura/callback", + "authorize": { + "url": "https://cloud.ouraring.com/oauth/authorize", + "query": { + "client_id": "client-id-1", + "redirect_uri": "https://healthlog.example/api/oura/callback", + "response_type": "code", + "scope": "daily spo2 heart_health stress workout heartrate personal", + "state": "state-nonce-1" + } + }, + "exchange": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "error": { + "name": "OuraApiError", + "message": "Oura exchangeCode error: 200", + "classification": "transient", + "httpStatus": 200, + "reason": "empty_token_body", + "verb": "exchangeCode", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "OuraApiError", + "message": "Oura exchangeCode error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "upstreamError": "invalid_grant", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + }, + { + "method": "POST", + "url": "https://api.ouraring.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + }, + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode.legacy", + "duration_ms": 0, + "status": 599, + "error": "http_599" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "OuraApiError", + "message": "Oura exchangeCode error: 400", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + }, + { + "method": "POST", + "url": "https://api.ouraring.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + }, + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode.legacy", + "duration_ms": 0, + "status": 599, + "error": "http_599" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "OuraApiError", + "message": "Oura exchangeCode error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "exchangeCode", + "upstreamError": "invalid_client", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + }, + { + "method": "POST", + "url": "https://api.ouraring.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + }, + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode.legacy", + "duration_ms": 0, + "status": 599, + "error": "http_599" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "OuraApiError", + "message": "Oura exchangeCode error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "exchangeCode", + "upstreamError": "rate_limited", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "OuraApiError", + "message": "Oura exchangeCode error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "exchangeCode", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + } + }, + "refresh": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "error": { + "name": "OuraApiError", + "message": "Oura refreshAccessToken error: 200", + "classification": "transient", + "httpStatus": 200, + "reason": "empty_token_body", + "verb": "refreshAccessToken", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "OuraApiError", + "message": "Oura refreshAccessToken error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "upstreamError": "invalid_grant", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + }, + { + "method": "POST", + "url": "https://api.ouraring.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + }, + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken.legacy", + "duration_ms": 0, + "status": 599, + "error": "http_599" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "OuraApiError", + "message": "Oura refreshAccessToken error: 400", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + }, + { + "method": "POST", + "url": "https://api.ouraring.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + }, + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken.legacy", + "duration_ms": 0, + "status": 599, + "error": "http_599" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "OuraApiError", + "message": "Oura refreshAccessToken error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "refreshAccessToken", + "upstreamError": "invalid_client", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + }, + { + "method": "POST", + "url": "https://api.ouraring.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + }, + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken.legacy", + "duration_ms": 0, + "status": 599, + "error": "http_599" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "OuraApiError", + "message": "Oura refreshAccessToken error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "refreshAccessToken", + "upstreamError": "rate_limited", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "OuraApiError", + "message": "Oura refreshAccessToken error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "refreshAccessToken", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + } + }, + "exchange_from_moi_refused": { + "error": { + "name": "OuraApiError", + "message": "Oura exchangeCode error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "upstreamError": "invalid_grant", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "exchange_legacy_fallback": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + }, + { + "method": "POST", + "url": "https://api.ouraring.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + }, + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode.legacy", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "exchange_both_refused": { + "error": { + "name": "OuraApiError", + "message": "Oura exchangeCode error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "exchangeCode", + "upstreamError": "invalid_client", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + }, + { + "method": "POST", + "url": "https://api.ouraring.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/oura/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + }, + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "exchangeCode.legacy", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "refresh_legacy_fallback": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + }, + { + "method": "POST", + "url": "https://api.ouraring.com/oauth/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 404, + "error": "http_404" + } + ] + }, + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken.legacy", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "refresh_outage_no_fallback": { + "error": { + "name": "OuraApiError", + "message": "Oura refreshAccessToken error: 502", + "classification": "transient", + "httpStatus": 502, + "reason": "http_502", + "verb": "refreshAccessToken", + "vendor": "Oura" + }, + "requests": [ + { + "method": "POST", + "url": "https://moi.ouraring.com/oauth/v2/ext/oauth-token", + "query": {}, + "headers": { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "client_secret": "client-secret-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "oura", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 502, + "error": "http_502" + } + ] + } + ], + "unansweredScript": 0 + } + }, + "polar": { + "redirectUri": "https://healthlog.example/api/polar/callback", + "authorize": { + "url": "https://flow.polar.com/oauth2/authorization", + "query": { + "client_id": "client-id-1", + "redirect_uri": "https://healthlog.example/api/polar/callback", + "response_type": "code", + "scope": "accesslink.read_all", + "state": "state-nonce-1" + } + }, + "exchange": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://polarremote.com/v2/oauth2/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/polar/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "polar", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "error": { + "name": "PolarApiError", + "message": "Polar exchangeCode error: 200", + "classification": "transient", + "httpStatus": 200, + "reason": "empty_token_body", + "verb": "exchangeCode", + "vendor": "Polar" + }, + "requests": [ + { + "method": "POST", + "url": "https://polarremote.com/v2/oauth2/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/polar/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "polar", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "PolarApiError", + "message": "Polar exchangeCode error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "upstreamError": "invalid_grant", + "vendor": "Polar" + }, + "requests": [ + { + "method": "POST", + "url": "https://polarremote.com/v2/oauth2/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/polar/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "polar", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "PolarApiError", + "message": "Polar exchangeCode error: 400", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "vendor": "Polar" + }, + "requests": [ + { + "method": "POST", + "url": "https://polarremote.com/v2/oauth2/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/polar/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "polar", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "PolarApiError", + "message": "Polar exchangeCode error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "exchangeCode", + "upstreamError": "invalid_client", + "vendor": "Polar" + }, + "requests": [ + { + "method": "POST", + "url": "https://polarremote.com/v2/oauth2/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/polar/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "polar", + "method": "exchangeCode", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "PolarApiError", + "message": "Polar exchangeCode error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "exchangeCode", + "upstreamError": "rate_limited", + "vendor": "Polar" + }, + "requests": [ + { + "method": "POST", + "url": "https://polarremote.com/v2/oauth2/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/polar/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "polar", + "method": "exchangeCode", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "PolarApiError", + "message": "Polar exchangeCode error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "exchangeCode", + "vendor": "Polar" + }, + "requests": [ + { + "method": "POST", + "url": "https://polarremote.com/v2/oauth2/token", + "query": {}, + "headers": { + "Accept": "application/json", + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "code": "code-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/polar/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "polar", + "method": "exchangeCode", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + } + } + }, + "google-health": { + "redirectUri": "https://healthlog.example/api/google-health/callback", + "authorize": { + "url": "https://accounts.google.com/o/oauth2/v2/auth", + "query": { + "access_type": "offline", + "client_id": "client-id-1", + "code_challenge": "pkce-challenge-1", + "code_challenge_method": "S256", + "prompt": "consent", + "redirect_uri": "https://healthlog.example/api/google-health/callback", + "response_type": "code", + "scope": "https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly https://www.googleapis.com/auth/googlehealth.sleep.readonly https://www.googleapis.com/auth/googlehealth.profile.readonly", + "state": "state-nonce-1" + } + }, + "exchange": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/google-health/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "result": null, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/google-health/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth exchangeCode error: 400 - invalid_grant", + "classification": "reauth_required", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "upstreamError": "invalid_grant", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/google-health/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth exchangeCode error: 400", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/google-health/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth exchangeCode error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "exchangeCode", + "upstreamError": "invalid_client", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/google-health/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "exchangeCode", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth exchangeCode error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "exchangeCode", + "upstreamError": "rate_limited", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/google-health/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "exchangeCode", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth exchangeCode error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "exchangeCode", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/google-health/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "exchangeCode", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + } + }, + "refresh": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "result": null, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth refreshAccessToken error: 400 - invalid_grant", + "classification": "reauth_required", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "upstreamError": "invalid_grant", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth refreshAccessToken error: 400", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth refreshAccessToken error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "refreshAccessToken", + "upstreamError": "invalid_client", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth refreshAccessToken error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "refreshAccessToken", + "upstreamError": "rate_limited", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "GoogleHealthApiError", + "message": "GoogleHealth refreshAccessToken error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "refreshAccessToken", + "vendor": "GoogleHealth" + }, + "requests": [ + { + "method": "POST", + "url": "https://oauth2.googleapis.com/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "google-health", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + } + } + }, + "fitbit": { + "redirectUri": "https://healthlog.example/api/fitbit/callback", + "authorize": { + "url": "https://www.fitbit.com/oauth2/authorize", + "query": { + "client_id": "client-id-1", + "code_challenge": "pkce-challenge-1", + "code_challenge_method": "S256", + "redirect_uri": "https://healthlog.example/api/fitbit/callback", + "response_type": "code", + "scope": "activity cardio_fitness heartrate oxygen_saturation profile respiratory_rate sleep weight", + "state": "state-nonce-1" + } + }, + "exchange": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/fitbit/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit exchangeCode error: 200", + "classification": "transient", + "httpStatus": 200, + "reason": "empty_token_body", + "verb": "exchangeCode", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/fitbit/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "exchangeCode", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit exchangeCode error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "upstreamError": "invalid_grant", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/fitbit/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit exchangeCode error: 400", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/fitbit/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit exchangeCode error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "exchangeCode", + "upstreamError": "invalid_client", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/fitbit/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "exchangeCode", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit exchangeCode error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "exchangeCode", + "upstreamError": "rate_limited", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/fitbit/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "exchangeCode", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit exchangeCode error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "exchangeCode", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/fitbit/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "exchangeCode", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + }, + "fitbit_errors": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit exchangeCode error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "exchangeCode", + "upstreamError": "invalid_grant", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "client_id": "client-id-1", + "code": "code-1", + "code_verifier": "pkce-verifier-1", + "grant_type": "authorization_code", + "redirect_uri": "https://healthlog.example/api/fitbit/callback" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "exchangeCode", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + } + }, + "refresh": { + "ok": { + "result": { + "access_token": "access-1", + "refresh_token": "refresh-1", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "granted" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "empty_body": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit refreshAccessToken error: 200", + "classification": "transient", + "httpStatus": 200, + "reason": "empty_token_body", + "verb": "refreshAccessToken", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 200 + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_grant": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit refreshAccessToken error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "upstreamError": "invalid_grant", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "strava_fault": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit refreshAccessToken error: 400", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + }, + "invalid_client": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit refreshAccessToken error: 401 - invalid_client", + "classification": "reauth_required", + "httpStatus": 401, + "reason": "http_401", + "verb": "refreshAccessToken", + "upstreamError": "invalid_client", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 401, + "error": "http_401" + } + ] + } + ], + "unansweredScript": 0 + }, + "rate_limited": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit refreshAccessToken error: 429 - rate_limited", + "classification": "transient", + "httpStatus": 429, + "reason": "http_429", + "verb": "refreshAccessToken", + "upstreamError": "rate_limited", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 429, + "error": "http_429" + } + ] + } + ], + "unansweredScript": 0 + }, + "outage": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit refreshAccessToken error: 503", + "classification": "transient", + "httpStatus": 503, + "reason": "http_503", + "verb": "refreshAccessToken", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 503, + "error": "http_503" + } + ] + } + ], + "unansweredScript": 0 + }, + "fitbit_errors": { + "error": { + "name": "FitbitApiError", + "message": "Fitbit refreshAccessToken error: 400 - invalid_grant", + "classification": "persistent", + "httpStatus": 400, + "reason": "http_400", + "verb": "refreshAccessToken", + "upstreamError": "invalid_grant", + "vendor": "Fitbit" + }, + "requests": [ + { + "method": "POST", + "url": "https://api.fitbit.com/oauth2/token", + "query": {}, + "headers": { + "Authorization": "Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x", + "Content-Type": "application/x-www-form-urlencoded" + }, + "form": { + "grant_type": "refresh_token", + "refresh_token": "refresh-0" + }, + "body": null + } + ], + "events": [ + { + "method": "addExternalCall", + "args": [ + { + "service": "fitbit", + "method": "refreshAccessToken", + "duration_ms": 0, + "status": 400, + "error": "http_400" + } + ] + } + ], + "unansweredScript": 0 + } + } + } +} diff --git a/src/lib/integrations/oauth-kit/__tests__/oauth-wire-parity.test.ts b/src/lib/integrations/oauth-kit/__tests__/oauth-wire-parity.test.ts new file mode 100644 index 000000000..65fca1c16 --- /dev/null +++ b/src/lib/integrations/oauth-kit/__tests__/oauth-wire-parity.test.ts @@ -0,0 +1,398 @@ +/** + * What every OAuth provider client puts on the wire, and what it makes of + * the answer, pinned in `__golden__/oauth-wire.json`. + * + * For each provider the authorization URL, the token exchange, the refresh + * and (Strava) the deauthorization are driven against a scripted token + * endpoint at the `safeFetch` seam: a good answer, an empty body, the refusal + * shapes each provider is known to send (an OAuth `invalid_grant`, Strava's + * `Fault`, a Withings status envelope), an unknown client, a rate limit and + * an outage. The golden records every request (method, URL with its query + * as a sorted map, headers, form body as a sorted map), the result or the + * error with its classification, and the external-call line the wide event + * received. + * + * The golden was written from the hand-written clients before they moved + * onto the shared OAuth client factory, so a green run is the proof that the + * factory speaks to each provider exactly as its own client did. Parameter + * order is not recorded: no authorization server reads it. + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +interface RecordedRequest { + method: string; + url: string; + query: Record; + headers: Record; + form: Record | null; + body: string | null; +} + +interface Scripted { + status: number; + body: string; + headers?: Record; +} + +const wire = vi.hoisted(() => ({ + requests: [] as RecordedRequest[], + answers: [] as Scripted[], + events: [] as Array<{ method: string; args: unknown[] }>, +})); + +vi.mock("@/lib/safe-fetch", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + safeFetch: async (input: string | URL, init: RequestInit = {}) => { + const url = new URL(String(input)); + const body = typeof init.body === "string" ? init.body : null; + const headers = Object.fromEntries( + Object.entries((init.headers ?? {}) as Record).sort( + ([a], [b]) => a.localeCompare(b), + ), + ); + const isForm = + Object.entries(headers).some( + ([name, value]) => + name.toLowerCase() === "content-type" && + value.includes("x-www-form-urlencoded"), + ) && body !== null; + wire.requests.push({ + method: init.method ?? "GET", + url: `${url.origin}${url.pathname}`, + query: sortedParams(url.searchParams), + headers, + form: isForm ? sortedParams(new URLSearchParams(body)) : null, + body: isForm ? null : body, + }); + const answer = wire.answers.shift() ?? { status: 599, body: "" }; + return new Response(answer.body === "" ? null : answer.body, { + status: answer.status, + headers: answer.headers, + }); + }, + }; +}); + +vi.mock("@/lib/logging/context", async (importOriginal) => { + const actual = await importOriginal(); + const recorder = new Proxy( + {}, + { + get: + (_target, method) => + (...args: unknown[]) => { + wire.events.push({ + method: String(method), + args: args.map((arg) => + arg && typeof arg === "object" && "duration_ms" in arg + ? { ...(arg as Record), duration_ms: 0 } + : arg, + ), + }); + }, + }, + ); + return { ...actual, getEvent: () => recorder }; +}); + +function sortedParams(params: URLSearchParams): Record { + return Object.fromEntries( + [...params.entries()].sort(([a], [b]) => a.localeCompare(b)), + ); +} + +const CREDS = { clientId: "client-id-1", clientSecret: "client-secret-1" }; +const STATE = "state-nonce-1"; +const CHALLENGE = "pkce-challenge-1"; +const VERIFIER = "pkce-verifier-1"; + +const json = (status: number, body: unknown, headers = {}): Scripted => ({ + status, + body: JSON.stringify(body), + headers: { "content-type": "application/json", ...headers }, +}); + +const TOKEN = { + access_token: "access-1", + refresh_token: "refresh-1", + expires_in: 3600, + token_type: "Bearer", + scope: "granted", +}; + +/** The answers every provider's token endpoint is driven through. */ +const STANDARD_ANSWERS: Record = { + ok: [json(200, TOKEN)], + empty_body: [{ status: 200, body: "" }], + invalid_grant: [json(400, { error: "invalid_grant" })], + strava_fault: [ + json(400, { + message: "Bad Request", + errors: [ + { resource: "RefreshToken", field: "refresh_token", code: "invalid" }, + ], + }), + ], + invalid_client: [json(401, { error: "invalid_client" })], + rate_limited: [json(429, { error: "rate_limited" }, { "retry-after": "30" })], + outage: [{ status: 503, body: "unavailable" }], +}; + +/** Withings answers HTTP 200 with a status inside the body. */ +const WITHINGS_ANSWERS: Record = { + ok: [json(200, { status: 0, body: { userid: "4711", ...TOKEN } })], + empty_body: [{ status: 200, body: "" }], + invalid_grant: [json(200, { status: 503, error: "Invalid Params" })], + revoked: [json(200, { status: 100, error: "The hash is missing" })], + invalid_client: [json(200, { status: 401, error: "invalid_client" })], + rate_limited: [json(200, { status: 601, error: "Too Many Requests" })], + outage: [{ status: 503, body: "unavailable" }], +}; + +type Call = () => Promise; + +interface ProviderUnderTest { + answers: Record; + redirectUri: () => string; + authorize: () => string; + exchange: Call; + refresh?: Call; + extra?: Record; +} + +/** + * How each provider's client is called. The calls are the only part of this + * file that follows the clients' API; the golden does not. + */ +async function providers(): Promise> { + const withings = await import("@/lib/withings/client"); + const whoop = await import("@/lib/whoop/client"); + const strava = await import("@/lib/strava/client"); + const oura = await import("@/lib/oura/client"); + const polar = await import("@/lib/polar/client"); + const google = await import("@/lib/google-health/client"); + const fitbit = await import("@/lib/fitbit/client"); + const MOI = "https://moi.ouraring.com/oauth/v2/ext/oauth-anonymous"; + return { + withings: { + answers: WITHINGS_ANSWERS, + redirectUri: withings.getWithingsRedirectUri, + authorize: () => withings.getAuthorizationUrl(STATE, CREDS), + exchange: () => withings.exchangeCode("code-1", CREDS), + refresh: () => withings.refreshAccessToken("refresh-0", CREDS), + }, + whoop: { + answers: STANDARD_ANSWERS, + redirectUri: whoop.getWhoopRedirectUri, + authorize: () => whoop.getAuthorizationUrl(STATE, CREDS), + exchange: () => whoop.exchangeCode("code-1", CREDS), + refresh: () => whoop.refreshAccessToken("refresh-0", CREDS), + }, + strava: { + answers: STANDARD_ANSWERS, + redirectUri: strava.getStravaRedirectUri, + authorize: () => strava.getAuthorizationUrl(STATE, CREDS), + exchange: () => strava.exchangeCode("code-1", CREDS), + refresh: () => strava.refreshAccessToken("refresh-0", CREDS), + extra: { + deauthorize_ok: { + answers: [json(200, { access_token: "access-1" })], + call: () => strava.deauthorize("access-1"), + }, + deauthorize_refused: { + answers: [json(401, { message: "Authorization Error" })], + call: () => strava.deauthorize("access-1"), + }, + }, + }, + oura: { + answers: STANDARD_ANSWERS, + redirectUri: oura.getOuraRedirectUri, + authorize: () => oura.getAuthorizationUrl(STATE, CREDS), + exchange: () => oura.exchangeCode("code-1", CREDS), + refresh: () => oura.refreshAccessToken("refresh-0", CREDS), + extra: { + exchange_from_moi_refused: { + answers: [json(400, { error: "invalid_grant" })], + call: () => oura.exchangeCode("code-1", CREDS, MOI), + }, + exchange_legacy_fallback: { + answers: [json(400, { error: "invalid_request" }), json(200, TOKEN)], + call: () => oura.exchangeCode("code-1", CREDS, null), + }, + exchange_both_refused: { + answers: [ + json(401, { error: "invalid_client" }), + json(400, { error: "invalid_grant" }), + ], + call: () => oura.exchangeCode("code-1", CREDS), + }, + refresh_legacy_fallback: { + answers: [json(404, { error: "not_found" }), json(200, TOKEN)], + call: () => oura.refreshAccessToken("refresh-0", CREDS), + }, + refresh_outage_no_fallback: { + answers: [{ status: 502, body: "" }], + call: () => oura.refreshAccessToken("refresh-0", CREDS), + }, + }, + }, + polar: { + answers: STANDARD_ANSWERS, + redirectUri: polar.getPolarRedirectUri, + authorize: () => polar.getAuthorizationUrl(STATE, CREDS), + exchange: () => polar.exchangeCode("code-1", CREDS), + }, + "google-health": { + answers: STANDARD_ANSWERS, + redirectUri: google.getGoogleHealthRedirectUri, + authorize: () => google.getAuthorizationUrl(STATE, CREDS, CHALLENGE), + exchange: () => google.exchangeCode("code-1", VERIFIER, CREDS), + refresh: () => google.refreshAccessToken("refresh-0", CREDS), + }, + fitbit: { + answers: { + ...STANDARD_ANSWERS, + fitbit_errors: [ + json(400, { + errors: [ + { errorType: "invalid_grant", message: "Refresh token invalid" }, + ], + success: false, + }), + ], + }, + redirectUri: fitbit.getFitbitRedirectUri, + authorize: () => fitbit.getAuthorizationUrl(STATE, CREDS, CHALLENGE), + exchange: () => fitbit.exchangeCode("code-1", VERIFIER, CREDS), + refresh: () => fitbit.refreshAccessToken("refresh-0", CREDS), + }, + }; +} + +function describeError(err: unknown): Record { + if (!(err instanceof Error)) return { thrown: String(err) }; + const fields: Record = { + name: err.name, + message: err.message, + }; + for (const key of [ + "classification", + "httpStatus", + "withingsStatus", + "reason", + "verb", + "upstreamError", + "vendor", + "retryAfterSeconds", + ]) { + const value = (err as unknown as Record)[key]; + if (value !== undefined) fields[key] = value; + } + return fields; +} + +async function drive(answers: Scripted[], call: Call) { + wire.requests.length = 0; + wire.events.length = 0; + wire.answers = answers.map((a) => ({ ...a })); + let outcome: Record; + try { + outcome = { result: (await call()) ?? null }; + } catch (err) { + outcome = { error: describeError(err) }; + } + return { + ...outcome, + requests: [...wire.requests], + events: [...wire.events], + unansweredScript: wire.answers.length, + }; +} + +beforeEach(() => { + vi.stubEnv("NEXT_PUBLIC_APP_URL", "https://healthlog.example"); + for (const name of [ + "WITHINGS_REDIRECT_URI", + "WHOOP_REDIRECT_URI", + "STRAVA_REDIRECT_URI", + "OURA_REDIRECT_URI", + "POLAR_REDIRECT_URI", + "GOOGLE_HEALTH_REDIRECT_URI", + "FITBIT_REDIRECT_URI", + ]) { + vi.stubEnv(name, ""); + } +}); + +describe("OAuth wire parity", () => { + it("speaks to every provider exactly as the golden records", async () => { + const transcript: Record = {}; + for (const [name, provider] of Object.entries(await providers())) { + const authorizeUrl = new URL(provider.authorize()); + const entry: Record = { + redirectUri: provider.redirectUri(), + authorize: { + url: `${authorizeUrl.origin}${authorizeUrl.pathname}`, + query: sortedParams(authorizeUrl.searchParams), + }, + }; + const calls: Array<[string, Call | undefined]> = [ + ["exchange", provider.exchange], + ["refresh", provider.refresh], + ]; + for (const [verb, call] of calls) { + if (!call) continue; + const outcomes: Record = {}; + for (const [scenario, answers] of Object.entries(provider.answers)) { + outcomes[scenario] = await drive(answers, call); + } + entry[verb] = outcomes; + } + for (const [scenario, extra] of Object.entries(provider.extra ?? {})) { + entry[scenario] = await drive(extra.answers, extra.call); + } + transcript[name] = entry; + } + + // The run must have reached every provider's token endpoint; an empty + // transcript would match an empty golden. + expect(Object.keys(transcript)).toHaveLength(7); + + await expect( + JSON.stringify(transcript, null, 2) + "\n", + ).toMatchFileSnapshot("./__golden__/oauth-wire.json"); + }); + + it("honours an explicit redirect URI from the environment", async () => { + vi.stubEnv( + "WHOOP_REDIRECT_URI", + "https://elsewhere.example/api/whoop/callback", + ); + vi.stubEnv( + "GOOGLE_HEALTH_REDIRECT_URI", + "https://healthlog.example/api/google-health/callback", + ); + const all = await providers(); + expect(all.whoop!.redirectUri()).toBe( + "https://elsewhere.example/api/whoop/callback", + ); + expect(all["google-health"]!.redirectUri()).toBe( + "https://healthlog.example/api/google-health/callback", + ); + vi.stubEnv( + "GOOGLE_HEALTH_REDIRECT_URI", + "https://elsewhere.example/api/google-health/callback", + ); + expect(() => all["google-health"]!.redirectUri()).toThrow( + /does not match NEXT_PUBLIC_APP_URL origin/, + ); + vi.stubEnv( + "FITBIT_REDIRECT_URI", + "http://healthlog.example/api/fitbit/callback", + ); + expect(() => all.fitbit!.redirectUri()).toThrow(/must be https/); + }); +});