From eca17d1c4c974f953a427a75af7f6113dacb5494 Mon Sep 17 00:00:00 2001 From: Clement Raymond Date: Mon, 15 Jun 2026 20:34:53 +0000 Subject: [PATCH] feat: validate client config on construction - Add validateConfig helper in sdkConfig.ts - Throw GuildPassError(INVALID_CONFIG) for missing/invalid apiUrl - Throw GuildPassError(INVALID_CONFIG) for non-positive timeoutMs - Call validateConfig at the top of GuildPassClient constructor - Add test suite covering all invalid and valid config cases Closes #21 --- src/client/GuildPassClient.ts | 3 ++- src/config/sdkConfig.ts | 20 ++++++++++++++++++++ tests/client.test.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/client/GuildPassClient.ts b/src/client/GuildPassClient.ts index 1b3ed85..f621429 100644 --- a/src/client/GuildPassClient.ts +++ b/src/client/GuildPassClient.ts @@ -3,7 +3,7 @@ import { AccessService } from '../access/access.service'; // GuildPass SDK: Import external module dependencies. import { DEFAULT_CONFIG } from '../config/defaultConfig'; // GuildPass SDK: Pull in package or module bindings. -import { GuildPassClientConfig } from '../config/sdkConfig'; +import { GuildPassClientConfig, validateConfig } from '../config/sdkConfig'; // GuildPass SDK: Import external module dependencies. import { ContractClient } from '../contracts/contractClient'; // GuildPass SDK: Pull in package or module bindings. @@ -41,6 +41,7 @@ export class GuildPassClient { // GuildPass SDK: Class member structure property or constructor. constructor(config: GuildPassClientConfig) { + validateConfig(config); // GuildPass SDK: Execution block boundary initialization. this.config = { ...DEFAULT_CONFIG, diff --git a/src/config/sdkConfig.ts b/src/config/sdkConfig.ts index 460b471..9a314f3 100644 --- a/src/config/sdkConfig.ts +++ b/src/config/sdkConfig.ts @@ -1,3 +1,6 @@ +import { GuildPassError } from '../errors/GuildPassError'; +import { GuildPassErrorCode } from '../errors/errorCodes'; + // GuildPass SDK: Exported component definition. export type GuildPassClientConfig = { apiUrl: string; @@ -8,3 +11,20 @@ export type GuildPassClientConfig = { timeoutMs?: number; // GuildPass SDK: End of logic containment structure block. }; + +export function validateConfig(config: GuildPassClientConfig): void { + if (!config.apiUrl) { + throw new GuildPassError('apiUrl is required', GuildPassErrorCode.INVALID_CONFIG); + } + try { + const url = new URL(config.apiUrl); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new Error(); + } + } catch { + throw new GuildPassError(`Invalid apiUrl: "${config.apiUrl}"`, GuildPassErrorCode.INVALID_CONFIG); + } + if (config.timeoutMs !== undefined && (typeof config.timeoutMs !== 'number' || config.timeoutMs <= 0)) { + throw new GuildPassError('timeoutMs must be a positive number', GuildPassErrorCode.INVALID_CONFIG); + } +} diff --git a/tests/client.test.ts b/tests/client.test.ts index 2595be4..75a0f51 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest'; // GuildPass SDK: Import external module dependencies. import { GuildPassClient } from '../src/client/GuildPassClient'; +import { GuildPassError } from '../src/errors/GuildPassError'; +import { GuildPassErrorCode } from '../src/errors/errorCodes'; // GuildPass SDK: Test suite container block. describe('GuildPassClient', () => { @@ -47,3 +49,32 @@ describe('GuildPassClient', () => { }); // GuildPass SDK: End of logic containment structure block. }); + +describe('GuildPassClient config validation', () => { + it('should throw when apiUrl is missing', () => { + expect(() => new GuildPassClient({ apiUrl: '' })) + .toThrow(GuildPassError); + expect(() => new GuildPassClient({ apiUrl: '' })) + .toThrow(expect.objectContaining({ code: GuildPassErrorCode.INVALID_CONFIG })); + }); + + it('should throw when apiUrl is an invalid URL', () => { + expect(() => new GuildPassClient({ apiUrl: 'not-a-url' })) + .toThrow(expect.objectContaining({ code: GuildPassErrorCode.INVALID_CONFIG })); + }); + + it('should throw when timeoutMs is zero', () => { + expect(() => new GuildPassClient({ apiUrl: 'https://api.guildpass.xyz', timeoutMs: 0 })) + .toThrow(expect.objectContaining({ code: GuildPassErrorCode.INVALID_CONFIG })); + }); + + it('should throw when timeoutMs is negative', () => { + expect(() => new GuildPassClient({ apiUrl: 'https://api.guildpass.xyz', timeoutMs: -1 })) + .toThrow(expect.objectContaining({ code: GuildPassErrorCode.INVALID_CONFIG })); + }); + + it('should not throw for valid config', () => { + expect(() => new GuildPassClient({ apiUrl: 'https://api.guildpass.xyz', timeoutMs: 5000 })) + .not.toThrow(); + }); +});