From fa972795f16a24fc1f43bd193fd0b2cfb3fd9e5b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 20:02:49 +0000 Subject: [PATCH] test: Cover the error guards and token predicates All six exported error guard functions and the token predicates had no test coverage. Add direct tests for each guard and predicate, including subclass and negative cases. Also correct the README claim that client options are deep merged, they are shallow merged, and stop excluding index files from the coverage report, which hid the per-route re-export surface. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01B8xeJm2Hd923k8uo6eoFd2 --- .c8rc.json | 1 - README.md | 3 +- test/seam/connect/error-guards.test.ts | 118 +++++++++++++++++++++++++ test/seam/connect/token.test.ts | 77 ++++++++++++++++ 4 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 test/seam/connect/error-guards.test.ts create mode 100644 test/seam/connect/token.test.ts diff --git a/.c8rc.json b/.c8rc.json index 8734c3d4..02dc2992 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -1,6 +1,5 @@ { "exclude": [ - "**/index.ts", "package/**/*.ts", "examples/**/*.ts", "**/*.test.ts", diff --git a/README.md b/README.md index 59a9fd52..b007d188 100644 --- a/README.md +++ b/README.md @@ -474,7 +474,8 @@ default. The Axios client and retry behavior may be configured with custom initiation options via [`axiosOptions`][axiosOptions] and [`axiosRetryOptions`][axiosRetryOptions]. -Options are deep merged with the default options. +Options are shallow merged with the default options: +each provided top-level option replaces the default value. By default, the SDK makes up to three attempts: the initial request and two retries. Retries are limited to `GET`, `HEAD`, `OPTIONS`, `PUT`, and `DELETE` diff --git a/test/seam/connect/error-guards.test.ts b/test/seam/connect/error-guards.test.ts new file mode 100644 index 00000000..9fcc72d0 --- /dev/null +++ b/test/seam/connect/error-guards.test.ts @@ -0,0 +1,118 @@ +import test from 'ava' + +import { + type ActionAttempt, + type FailedActionAttempt, + isSeamActionAttemptError, + isSeamActionAttemptFailedError, + isSeamActionAttemptTimeoutError, + isSeamHttpApiError, + isSeamHttpInvalidInputError, + isSeamHttpUnauthorizedError, + SeamActionAttemptError, + SeamActionAttemptFailedError, + SeamActionAttemptTimeoutError, + SeamHttpApiError, + SeamHttpInvalidInputError, + SeamHttpUnauthorizedError, +} from '@seamapi/http/connect' + +const apiError = new SeamHttpApiError( + { type: 'device_not_found', message: 'Device not found' }, + 404, + 'request-1', +) + +const unauthorizedError = new SeamHttpUnauthorizedError('request-1') + +const invalidInputError = new SeamHttpInvalidInputError( + { + type: 'invalid_input', + message: 'Invalid input', + validation_errors: { device_id: { _errors: ['Required'] } }, + }, + 400, + 'request-1', +) + +const pendingActionAttempt = { + action_attempt_id: 'e2192660-0e45-4a11-9800-eb4d086cca09', + action_type: 'UNLOCK_DOOR', + status: 'pending', + error: null, + result: null, +} as unknown as ActionAttempt + +const failedActionAttempt = { + ...pendingActionAttempt, + status: 'error', + error: { message: 'Failed', type: 'foo' }, +} as unknown as FailedActionAttempt + +const actionAttemptError = new SeamActionAttemptError( + 'Something happened', + pendingActionAttempt, +) + +const actionAttemptFailedError = new SeamActionAttemptFailedError( + failedActionAttempt, +) + +const actionAttemptTimeoutError = new SeamActionAttemptTimeoutError( + pendingActionAttempt, + 100, +) + +const unrelatedValues = [new Error('unrelated'), null, undefined, {}, 'error'] + +test('isSeamHttpApiError: matches every Seam API error', (t) => { + t.true(isSeamHttpApiError(apiError)) + t.true(isSeamHttpApiError(unauthorizedError)) + t.true(isSeamHttpApiError(invalidInputError)) + t.false(isSeamHttpApiError(actionAttemptError)) + for (const value of unrelatedValues) t.false(isSeamHttpApiError(value)) +}) + +test('isSeamHttpUnauthorizedError: matches only the unauthorized error', (t) => { + t.true(isSeamHttpUnauthorizedError(unauthorizedError)) + t.false(isSeamHttpUnauthorizedError(apiError)) + t.false(isSeamHttpUnauthorizedError(invalidInputError)) + for (const value of unrelatedValues) { + t.false(isSeamHttpUnauthorizedError(value)) + } +}) + +test('isSeamHttpInvalidInputError: matches only the invalid input error', (t) => { + t.true(isSeamHttpInvalidInputError(invalidInputError)) + t.false(isSeamHttpInvalidInputError(apiError)) + t.false(isSeamHttpInvalidInputError(unauthorizedError)) + for (const value of unrelatedValues) { + t.false(isSeamHttpInvalidInputError(value)) + } +}) + +test('isSeamActionAttemptError: matches every action attempt error', (t) => { + t.true(isSeamActionAttemptError(actionAttemptError)) + t.true(isSeamActionAttemptError(actionAttemptFailedError)) + t.true(isSeamActionAttemptError(actionAttemptTimeoutError)) + t.false(isSeamActionAttemptError(apiError)) + for (const value of unrelatedValues) t.false(isSeamActionAttemptError(value)) +}) + +test('isSeamActionAttemptFailedError: matches only the failed error', (t) => { + t.true(isSeamActionAttemptFailedError(actionAttemptFailedError)) + t.false(isSeamActionAttemptFailedError(actionAttemptError)) + t.false(isSeamActionAttemptFailedError(actionAttemptTimeoutError)) + for (const value of unrelatedValues) { + t.false(isSeamActionAttemptFailedError(value)) + } +}) + +test('isSeamActionAttemptTimeoutError: matches only the timeout error', (t) => { + t.true(isSeamActionAttemptTimeoutError(actionAttemptTimeoutError)) + t.false(isSeamActionAttemptTimeoutError(actionAttemptError)) + t.false(isSeamActionAttemptTimeoutError(actionAttemptFailedError)) + for (const value of unrelatedValues) { + t.false(isSeamActionAttemptTimeoutError(value)) + } +}) diff --git a/test/seam/connect/token.test.ts b/test/seam/connect/token.test.ts new file mode 100644 index 00000000..d737f3aa --- /dev/null +++ b/test/seam/connect/token.test.ts @@ -0,0 +1,77 @@ +import test from 'ava' + +import { + isApiKey, + isClientSessionToken, + isConsoleSessionToken, + isPersonalAccessToken, + isPublishableKey, +} from '@seamapi/http/connect' + +import { isAccessToken, isJwt, isSeamToken } from 'lib/token.js' + +const apiKey = 'seam_apikey1_token' +const accessToken = 'seam_at1_token' +const clientSessionToken = 'seam_cst1_token' +const publishableKey = 'seam_pk1_token' +const jwt = 'ey1.ey2.token' +const unknownToken = 'some-other-token' + +test('isApiKey: only matches an api key', (t) => { + t.true(isApiKey(apiKey)) + t.false(isApiKey(accessToken)) + t.false(isApiKey(clientSessionToken)) + t.false(isApiKey(publishableKey)) + t.false(isApiKey(jwt)) + t.false(isApiKey(unknownToken)) +}) + +test('isAccessToken: matches the access token prefix', (t) => { + t.true(isAccessToken(accessToken)) + t.false(isAccessToken(apiKey)) + t.false(isAccessToken(jwt)) + t.false(isAccessToken(unknownToken)) +}) + +test('isPersonalAccessToken: matches an access token', (t) => { + t.true(isPersonalAccessToken(accessToken)) + t.false(isPersonalAccessToken(apiKey)) + t.false(isPersonalAccessToken(clientSessionToken)) + t.false(isPersonalAccessToken(unknownToken)) +}) + +test('isClientSessionToken: matches the client session token prefix', (t) => { + t.true(isClientSessionToken(clientSessionToken)) + t.false(isClientSessionToken(apiKey)) + t.false(isClientSessionToken(publishableKey)) + t.false(isClientSessionToken(unknownToken)) +}) + +test('isPublishableKey: matches the publishable key prefix', (t) => { + t.true(isPublishableKey(publishableKey)) + t.false(isPublishableKey(apiKey)) + t.false(isPublishableKey(clientSessionToken)) + t.false(isPublishableKey(unknownToken)) +}) + +test('isJwt: matches the jwt prefix', (t) => { + t.true(isJwt(jwt)) + t.false(isJwt(apiKey)) + t.false(isJwt(unknownToken)) +}) + +test('isConsoleSessionToken: matches a jwt', (t) => { + t.true(isConsoleSessionToken(jwt)) + t.false(isConsoleSessionToken(apiKey)) + t.false(isConsoleSessionToken(accessToken)) + t.false(isConsoleSessionToken(unknownToken)) +}) + +test('isSeamToken: matches the seam token prefix', (t) => { + t.true(isSeamToken(apiKey)) + t.true(isSeamToken(accessToken)) + t.true(isSeamToken(clientSessionToken)) + t.true(isSeamToken(publishableKey)) + t.false(isSeamToken(jwt)) + t.false(isSeamToken(unknownToken)) +})