From 244b49f65e055ebde67fd9c977a0f49a30979227 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sun, 8 Mar 2026 22:08:30 +0100 Subject: [PATCH 1/9] feat: Auto-configure security from OpenAPI securitySchemes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #337 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../generators/typescript/channels/openapi.ts | 7 +- .../channels/protocols/http/fetch.ts | 450 ++++++++++-- .../generators/typescript/channels/types.ts | 10 + src/codegen/inputs/openapi/security.ts | 322 +++++++++ .../__snapshots__/channels.spec.ts.snap | 24 +- .../protocols/http/fetch-security.spec.ts | 230 +++++++ test/codegen/inputs/openapi/security.spec.ts | 645 ++++++++++++++++++ .../src/openapi/channels/http_client.ts | 24 +- .../http_client/security_schemes.spec.ts | 235 +++++++ 9 files changed, 1834 insertions(+), 113 deletions(-) create mode 100644 src/codegen/inputs/openapi/security.ts create mode 100644 test/codegen/generators/typescript/channels/protocols/http/fetch-security.spec.ts create mode 100644 test/codegen/inputs/openapi/security.spec.ts create mode 100644 test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts diff --git a/src/codegen/generators/typescript/channels/openapi.ts b/src/codegen/generators/typescript/channels/openapi.ts index 1fed6596..2099bd9a 100644 --- a/src/codegen/generators/typescript/channels/openapi.ts +++ b/src/codegen/generators/typescript/channels/openapi.ts @@ -21,6 +21,7 @@ import {getMessageTypeAndModule} from './utils'; import {pascalCase} from '../utils'; import {createMissingInputDocumentError} from '../../../errors'; import {resolveImportExtension} from '../../../utils'; +import {extractSecuritySchemes} from '../../../inputs/openapi/security'; type OpenAPIDocument = | OpenAPIV3.Document @@ -75,6 +76,9 @@ export async function generateTypeScriptChannelsForOpenAPI( const {openapiDocument} = validateOpenAPIContext(context); + // Extract security schemes from the OpenAPI document + const securitySchemes = extractSecuritySchemes(openapiDocument); + // Collect dependencies const deps = protocolDependencies['http_client']; const importExtension = resolveImportExtension( @@ -98,8 +102,9 @@ export async function generateTypeScriptChannelsForOpenAPI( ); // Generate common types once (stateless check) + // Pass security schemes to generate only relevant auth types if (protocolCodeFunctions['http_client'].length === 0 && renders.length > 0) { - const commonTypesCode = renderHttpCommonTypes(); + const commonTypesCode = renderHttpCommonTypes(securitySchemes); protocolCodeFunctions['http_client'].unshift(commonTypesCode); } diff --git a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts index 03f1e7ec..94c56e39 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts @@ -1,12 +1,387 @@ import {HttpRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; -import {ChannelFunctionTypes, RenderHttpParameters} from '../../types'; +import { + ChannelFunctionTypes, + RenderHttpParameters, + ExtractedSecurityScheme +} from '../../types'; + +// Re-export for use by other modules +export {ExtractedSecurityScheme}; + +/** + * Determines which auth types are needed based on security schemes. + */ +interface AuthTypeRequirements { + bearer: boolean; + basic: boolean; + apiKey: boolean; + oauth2: boolean; + apiKeySchemes: ExtractedSecurityScheme[]; + oauth2Schemes: ExtractedSecurityScheme[]; +} + +/** + * Analyzes security schemes to determine which auth types are needed. + */ +function analyzeSecuritySchemes( + schemes: ExtractedSecurityScheme[] | undefined +): AuthTypeRequirements { + // No schemes = backward compatibility mode, generate all types + if (!schemes || schemes.length === 0) { + return { + bearer: true, + basic: true, + apiKey: true, + oauth2: true, + apiKeySchemes: [], + oauth2Schemes: [] + }; + } + + const requirements: AuthTypeRequirements = { + bearer: false, + basic: false, + apiKey: false, + oauth2: false, + apiKeySchemes: [], + oauth2Schemes: [] + }; + + for (const scheme of schemes) { + switch (scheme.type) { + case 'apiKey': + requirements.apiKey = true; + requirements.apiKeySchemes.push(scheme); + break; + case 'http': + if (scheme.httpScheme === 'bearer') { + requirements.bearer = true; + } else if (scheme.httpScheme === 'basic') { + requirements.basic = true; + } + break; + case 'oauth2': + case 'openIdConnect': + requirements.oauth2 = true; + requirements.oauth2Schemes.push(scheme); + break; + } + } + + return requirements; +} + +/** + * Generates the BearerAuth interface. + */ +function renderBearerAuthInterface(): string { + return `/** + * Bearer token authentication configuration + */ +export interface BearerAuth { + type: 'bearer'; + token: string; +}`; +} + +/** + * Generates the BasicAuth interface. + */ +function renderBasicAuthInterface(): string { + return `/** + * Basic authentication configuration (username/password) + */ +export interface BasicAuth { + type: 'basic'; + username: string; + password: string; +}`; +} + +/** + * Generates the ApiKeyAuth interface with optional pre-populated defaults from spec. + */ +function renderApiKeyAuthInterface( + apiKeySchemes: ExtractedSecurityScheme[] +): string { + // If there's exactly one apiKey scheme, we can provide defaults + let defaultName = 'X-API-Key'; + let defaultIn: string = 'header'; + + if (apiKeySchemes.length === 1) { + defaultName = apiKeySchemes[0].apiKeyName || defaultName; + defaultIn = apiKeySchemes[0].apiKeyIn || defaultIn; + } + + // For cookie support + const inType = apiKeySchemes.some((s) => s.apiKeyIn === 'cookie') + ? "'header' | 'query' | 'cookie'" + : "'header' | 'query'"; + + return `/** + * API key authentication configuration + */ +export interface ApiKeyAuth { + type: 'apiKey'; + key: string; + name?: string; // Name of the API key parameter (default: '${defaultName}') + in?: ${inType}; // Where to place the API key (default: '${defaultIn}') +}`; +} + +/** + * Extracts the tokenUrl from OAuth2 flows. + */ +function extractTokenUrl( + flows: NonNullable +): string | undefined { + return ( + flows.clientCredentials?.tokenUrl || + flows.password?.tokenUrl || + flows.authorizationCode?.tokenUrl + ); +} + +/** + * Extracts the authorizationUrl from OAuth2 flows. + */ +function extractAuthorizationUrl( + flows: NonNullable +): string | undefined { + return ( + flows.implicit?.authorizationUrl || + flows.authorizationCode?.authorizationUrl + ); +} + +/** + * Collects all scopes from OAuth2 flows. + */ +function collectScopes( + flows: NonNullable +): Set { + const allScopes = new Set(); + const flowTypes = [ + flows.implicit, + flows.password, + flows.clientCredentials, + flows.authorizationCode + ]; + + for (const flow of flowTypes) { + if (flow?.scopes) { + Object.keys(flow.scopes).forEach((s) => allScopes.add(s)); + } + } + + return allScopes; +} + +interface OAuth2DocComments { + tokenUrlComment: string; + authorizationUrlComment: string; + scopesComment: string; +} + +/** + * Formats scopes into a documentation comment. + */ +function formatScopesComment(scopes: Set): string { + if (scopes.size === 0) { + return ''; + } + const scopeList = Array.from(scopes).slice(0, 3).join(', '); + const suffix = scopes.size > 3 ? '...' : ''; + return ` Available: ${scopeList}${suffix}`; +} + +/** + * Extracts documentation comments from a single OAuth2 scheme. + */ +function extractSchemeComments( + scheme: ExtractedSecurityScheme, + existing: OAuth2DocComments +): OAuth2DocComments { + if (scheme.openIdConnectUrl) { + return { + ...existing, + tokenUrlComment: `OpenID Connect URL: '${scheme.openIdConnectUrl}'` + }; + } + + if (!scheme.oauth2Flows) { + return existing; + } + + const tokenUrl = extractTokenUrl(scheme.oauth2Flows); + const authUrl = extractAuthorizationUrl(scheme.oauth2Flows); + const allScopes = collectScopes(scheme.oauth2Flows); + + return { + tokenUrlComment: tokenUrl + ? `default: '${tokenUrl}'` + : existing.tokenUrlComment, + authorizationUrlComment: authUrl + ? ` Authorization URL: '${authUrl}'` + : existing.authorizationUrlComment, + scopesComment: formatScopesComment(allScopes) || existing.scopesComment + }; +} + +/** + * Extracts documentation comments from OAuth2 schemes. + */ +function extractOAuth2DocComments( + oauth2Schemes: ExtractedSecurityScheme[] +): OAuth2DocComments { + const initial: OAuth2DocComments = { + tokenUrlComment: + 'required for client_credentials/password flows and token refresh', + authorizationUrlComment: '', + scopesComment: '' + }; + + return oauth2Schemes.reduce( + (acc, scheme) => extractSchemeComments(scheme, acc), + initial + ); +} + +/** + * Generates the OAuth2Auth interface with optional pre-populated values from spec. + */ +function renderOAuth2AuthInterface( + oauth2Schemes: ExtractedSecurityScheme[] +): string { + const {tokenUrlComment, authorizationUrlComment, scopesComment} = + extractOAuth2DocComments(oauth2Schemes); + + const flowsInfo = authorizationUrlComment + ? `\n *${authorizationUrlComment}` + : ''; + + return `/** + * OAuth2 authentication configuration + * + * Supports server-side flows only: + * - client_credentials: Server-to-server authentication + * - password: Resource owner password credentials (legacy, not recommended) + * - Pre-obtained accessToken: For tokens obtained via browser-based flows + * + * For browser-based flows (implicit, authorization_code), obtain the token + * separately and pass it as accessToken.${flowsInfo} + */ +export interface OAuth2Auth { + type: 'oauth2'; + /** Pre-obtained access token (required if not using a server-side flow) */ + accessToken?: string; + /** Refresh token for automatic token renewal on 401 */ + refreshToken?: string; + /** Token endpoint URL (${tokenUrlComment}) */ + tokenUrl?: string; + /** Client ID (required for flows and token refresh) */ + clientId?: string; + /** Client secret (optional, depends on OAuth provider) */ + clientSecret?: string; + /** Requested scopes${scopesComment} */ + scopes?: string[]; + /** Server-side flow type */ + flow?: 'password' | 'client_credentials'; + /** Username for password flow */ + username?: string; + /** Password for password flow */ + password?: string; + /** Callback when tokens are refreshed (for caching/persistence) */ + onTokenRefresh?: (newTokens: TokenResponse) => void; +}`; +} + +/** + * Generates the AuthConfig union type based on which auth types are needed. + */ +function renderAuthConfigType(requirements: AuthTypeRequirements): string { + const types: string[] = []; + + if (requirements.bearer) { + types.push('BearerAuth'); + } + if (requirements.basic) { + types.push('BasicAuth'); + } + if (requirements.apiKey) { + types.push('ApiKeyAuth'); + } + if (requirements.oauth2) { + types.push('OAuth2Auth'); + } + + // If no types, default to all (shouldn't happen but be safe) + if (types.length === 0) { + return 'export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth;'; + } + + return `/** + * Union type for all authentication methods - provides autocomplete support + */ +export type AuthConfig = ${types.join(' | ')};`; +} + +/** + * Generates the security configuration types based on extracted security schemes. + */ +function renderSecurityTypes( + schemes: ExtractedSecurityScheme[] | undefined +): string { + const requirements = analyzeSecuritySchemes(schemes); + + const parts: string[] = [ + '// ============================================================================', + '// Security Configuration Types - Grouped for better autocomplete', + '// ============================================================================', + '' + ]; + + // Only generate interfaces for required auth types + if (requirements.bearer) { + parts.push(renderBearerAuthInterface()); + parts.push(''); + } + + if (requirements.basic) { + parts.push(renderBasicAuthInterface()); + parts.push(''); + } + + if (requirements.apiKey) { + parts.push(renderApiKeyAuthInterface(requirements.apiKeySchemes)); + parts.push(''); + } + + if (requirements.oauth2) { + parts.push(renderOAuth2AuthInterface(requirements.oauth2Schemes)); + parts.push(''); + } + + // Add the AuthConfig union type + parts.push(renderAuthConfigType(requirements)); + + return parts.join('\n'); +} /** * Generates common types and helper functions shared across all HTTP client functions. * This should be called once per protocol generation to avoid code duplication. + * + * @param securitySchemes - Optional security schemes extracted from OpenAPI. + * When provided, only relevant auth types are generated. + * When undefined/empty, all auth types are generated for backward compatibility. */ -export function renderHttpCommonTypes(): string { +export function renderHttpCommonTypes( + securitySchemes?: ExtractedSecurityScheme[] +): string { + const securityTypes = renderSecurityTypes(securitySchemes); + return `// ============================================================================ // Common Types - Shared across all HTTP client functions // ============================================================================ @@ -88,76 +463,7 @@ export interface TokenResponse { expiresIn?: number; } -// ============================================================================ -// Security Configuration Types - Grouped for better autocomplete -// ============================================================================ - -/** - * Bearer token authentication configuration - */ -export interface BearerAuth { - type: 'bearer'; - token: string; -} - -/** - * Basic authentication configuration (username/password) - */ -export interface BasicAuth { - type: 'basic'; - username: string; - password: string; -} - -/** - * API key authentication configuration - */ -export interface ApiKeyAuth { - type: 'apiKey'; - key: string; - name?: string; // Name of the API key parameter (default: 'X-API-Key') - in?: 'header' | 'query'; // Where to place the API key (default: 'header') -} - -/** - * OAuth2 authentication configuration - * - * Supports server-side flows only: - * - client_credentials: Server-to-server authentication - * - password: Resource owner password credentials (legacy, not recommended) - * - Pre-obtained accessToken: For tokens obtained via browser-based flows - * - * For browser-based flows (implicit, authorization_code), obtain the token - * separately and pass it as accessToken. - */ -export interface OAuth2Auth { - type: 'oauth2'; - /** Pre-obtained access token (required if not using a server-side flow) */ - accessToken?: string; - /** Refresh token for automatic token renewal on 401 */ - refreshToken?: string; - /** Token endpoint URL (required for client_credentials/password flows and token refresh) */ - tokenUrl?: string; - /** Client ID (required for flows and token refresh) */ - clientId?: string; - /** Client secret (optional, depends on OAuth provider) */ - clientSecret?: string; - /** Requested scopes */ - scopes?: string[]; - /** Server-side flow type */ - flow?: 'password' | 'client_credentials'; - /** Username for password flow */ - username?: string; - /** Password for password flow */ - password?: string; - /** Callback when tokens are refreshed (for caching/persistence) */ - onTokenRefresh?: (newTokens: TokenResponse) => void; -} - -/** - * Union type for all authentication methods - provides autocomplete support - */ -export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth; +${securityTypes} // ============================================================================ // Pagination Types diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index 234bb18b..3206440c 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -6,6 +6,10 @@ import {TypeScriptPayloadRenderType} from '../payloads'; import {TypeScriptParameterRenderType} from '../parameters'; import {ConstrainedObjectModel} from '@asyncapi/modelina'; import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; +import {ExtractedSecurityScheme} from '../../../inputs/openapi/security'; + +// Re-export for convenience +export {ExtractedSecurityScheme}; export enum ChannelFunctionTypes { NATS_JETSTREAM_PUBLISH = 'nats_jetstream_publish', @@ -247,6 +251,12 @@ export interface RenderHttpParameters { * When true, use unmarshalByStatusCode(json, statusCode) instead of unmarshal(json). */ includesStatusCodes?: boolean; + /** + * Security schemes extracted from the OpenAPI document. + * When provided, only auth types for these schemes will be generated. + * When undefined or empty, all auth types are generated for backward compatibility. + */ + securitySchemes?: ExtractedSecurityScheme[]; } export type SupportedProtocols = diff --git a/src/codegen/inputs/openapi/security.ts b/src/codegen/inputs/openapi/security.ts new file mode 100644 index 00000000..f2fcfc77 --- /dev/null +++ b/src/codegen/inputs/openapi/security.ts @@ -0,0 +1,322 @@ +/** + * Extracts security scheme information from OpenAPI 2.0/3.x documents. + * Converts security definitions to a normalized internal format. + */ +import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; + +/** + * Normalized security scheme extracted from OpenAPI documents. + * Supports OpenAPI 3.x securitySchemes and Swagger 2.0 securityDefinitions. + */ +export interface ExtractedSecurityScheme { + /** The name/key of the security scheme as defined in the spec */ + name: string; + /** Security scheme type */ + type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect'; + + /** For apiKey type: the name of the key parameter */ + apiKeyName?: string; + /** For apiKey type: where to place the key */ + apiKeyIn?: 'header' | 'query' | 'cookie'; + + /** For http type: the authentication scheme (bearer, basic, etc.) */ + httpScheme?: 'bearer' | 'basic' | string; + /** For http bearer: the format of the bearer token */ + bearerFormat?: string; + + /** For oauth2 type: the available flows */ + oauth2Flows?: { + implicit?: { + authorizationUrl: string; + scopes: Record; + }; + password?: { + tokenUrl: string; + scopes: Record; + }; + clientCredentials?: { + tokenUrl: string; + scopes: Record; + }; + authorizationCode?: { + authorizationUrl: string; + tokenUrl: string; + scopes: Record; + }; + }; + + /** For openIdConnect type: the OpenID Connect discovery URL */ + openIdConnectUrl?: string; +} + +type OpenAPIDocument = + | OpenAPIV3.Document + | OpenAPIV2.Document + | OpenAPIV3_1.Document; +type OpenAPIOperation = + | OpenAPIV3.OperationObject + | OpenAPIV2.OperationObject + | OpenAPIV3_1.OperationObject; + +/** + * Extracts security schemes from an OpenAPI document. + * Handles both OpenAPI 3.x (components.securitySchemes) and + * Swagger 2.0 (securityDefinitions) formats. + */ +export function extractSecuritySchemes( + document: OpenAPIDocument +): ExtractedSecurityScheme[] { + // Check if OpenAPI 3.x document + if ('openapi' in document) { + return extractOpenAPI3SecuritySchemes( + document as OpenAPIV3.Document | OpenAPIV3_1.Document + ); + } + + // Check if Swagger 2.0 document + if ('swagger' in document) { + return extractSwagger2SecuritySchemes(document as OpenAPIV2.Document); + } + + return []; +} + +/** + * Extracts security schemes from OpenAPI 3.x documents. + */ +function extractOpenAPI3SecuritySchemes( + document: OpenAPIV3.Document | OpenAPIV3_1.Document +): ExtractedSecurityScheme[] { + const securitySchemes = document.components?.securitySchemes; + if (!securitySchemes) { + return []; + } + + const schemes: ExtractedSecurityScheme[] = []; + + for (const [name, scheme] of Object.entries(securitySchemes)) { + // Skip $ref - should be dereferenced already + if ('$ref' in scheme) { + continue; + } + + const securityScheme = scheme as OpenAPIV3.SecuritySchemeObject; + const extracted = extractOpenAPI3Scheme(name, securityScheme); + if (extracted) { + schemes.push(extracted); + } + } + + return schemes; +} + +/** + * Extracts a single OpenAPI 3.x security scheme. + */ +function extractOpenAPI3Scheme( + name: string, + scheme: OpenAPIV3.SecuritySchemeObject +): ExtractedSecurityScheme | undefined { + switch (scheme.type) { + case 'apiKey': + return { + name, + type: 'apiKey', + apiKeyName: scheme.name, + apiKeyIn: scheme.in as 'header' | 'query' | 'cookie' + }; + + case 'http': + return { + name, + type: 'http', + httpScheme: scheme.scheme as 'bearer' | 'basic', + bearerFormat: scheme.bearerFormat + }; + + case 'oauth2': + return { + name, + type: 'oauth2', + oauth2Flows: extractOAuth2Flows(scheme.flows) + }; + + case 'openIdConnect': + return { + name, + type: 'openIdConnect', + openIdConnectUrl: scheme.openIdConnectUrl + }; + + default: + return undefined; + } +} + +/** + * Extracts OAuth2 flows from OpenAPI 3.x OAuth2 security scheme. + */ +function extractOAuth2Flows( + flows: OpenAPIV3.OAuth2SecurityScheme['flows'] +): ExtractedSecurityScheme['oauth2Flows'] { + const result: ExtractedSecurityScheme['oauth2Flows'] = {}; + + if (flows.implicit) { + result.implicit = { + authorizationUrl: flows.implicit.authorizationUrl, + scopes: flows.implicit.scopes || {} + }; + } + + if (flows.password) { + result.password = { + tokenUrl: flows.password.tokenUrl, + scopes: flows.password.scopes || {} + }; + } + + if (flows.clientCredentials) { + result.clientCredentials = { + tokenUrl: flows.clientCredentials.tokenUrl, + scopes: flows.clientCredentials.scopes || {} + }; + } + + if (flows.authorizationCode) { + result.authorizationCode = { + authorizationUrl: flows.authorizationCode.authorizationUrl, + tokenUrl: flows.authorizationCode.tokenUrl, + scopes: flows.authorizationCode.scopes || {} + }; + } + + return result; +} + +/** + * Extracts security definitions from Swagger 2.0 documents. + */ +function extractSwagger2SecuritySchemes( + document: OpenAPIV2.Document +): ExtractedSecurityScheme[] { + const securityDefinitions = document.securityDefinitions; + if (!securityDefinitions) { + return []; + } + + const schemes: ExtractedSecurityScheme[] = []; + + for (const [name, definition] of Object.entries(securityDefinitions)) { + const extracted = extractSwagger2Scheme(name, definition); + if (extracted) { + schemes.push(extracted); + } + } + + return schemes; +} + +/** + * Extracts a single Swagger 2.0 security definition. + */ +function extractSwagger2Scheme( + name: string, + definition: OpenAPIV2.SecuritySchemeObject +): ExtractedSecurityScheme | undefined { + switch (definition.type) { + case 'apiKey': + return { + name, + type: 'apiKey', + apiKeyName: definition.name, + apiKeyIn: definition.in as 'header' | 'query' + }; + + case 'basic': + // Swagger 2.0 has a separate 'basic' type which maps to http basic + return { + name, + type: 'http', + httpScheme: 'basic' + }; + + case 'oauth2': + return { + name, + type: 'oauth2', + oauth2Flows: extractSwagger2OAuth2Flow(definition) + }; + + default: + return undefined; + } +} + +/** + * Extracts OAuth2 flow from Swagger 2.0 format. + * Swagger 2.0 uses single 'flow' field instead of 'flows' object. + */ +function extractSwagger2OAuth2Flow( + definition: OpenAPIV2.SecuritySchemeOauth2 +): ExtractedSecurityScheme['oauth2Flows'] { + const result: ExtractedSecurityScheme['oauth2Flows'] = {}; + const scopes = definition.scopes || {}; + + switch (definition.flow) { + case 'implicit': + result.implicit = { + authorizationUrl: definition.authorizationUrl || '', + scopes + }; + break; + + case 'password': + result.password = { + tokenUrl: definition.tokenUrl || '', + scopes + }; + break; + + case 'application': + // Swagger 2.0 'application' maps to OpenAPI 3.x 'clientCredentials' + result.clientCredentials = { + tokenUrl: definition.tokenUrl || '', + scopes + }; + break; + + case 'accessCode': + // Swagger 2.0 'accessCode' maps to OpenAPI 3.x 'authorizationCode' + result.authorizationCode = { + authorizationUrl: definition.authorizationUrl || '', + tokenUrl: definition.tokenUrl || '', + scopes + }; + break; + } + + return result; +} + +/** + * Extracts security requirement names from an OpenAPI operation. + * Returns the unique security scheme names that the operation requires. + */ +export function getOperationSecurityRequirements( + operation: OpenAPIOperation +): string[] { + const security = operation.security; + if (!security || security.length === 0) { + return []; + } + + const requirements = new Set(); + + for (const requirement of security) { + for (const schemeName of Object.keys(requirement)) { + requirements.add(schemeName); + } + } + + return Array.from(requirements); +} diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 52a490e3..ada7fa1d 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -99,30 +99,13 @@ export interface TokenResponse { // Security Configuration Types - Grouped for better autocomplete // ============================================================================ -/** - * Bearer token authentication configuration - */ -export interface BearerAuth { - type: 'bearer'; - token: string; -} - -/** - * Basic authentication configuration (username/password) - */ -export interface BasicAuth { - type: 'basic'; - username: string; - password: string; -} - /** * API key authentication configuration */ export interface ApiKeyAuth { type: 'apiKey'; key: string; - name?: string; // Name of the API key parameter (default: 'X-API-Key') + name?: string; // Name of the API key parameter (default: 'api_key') in?: 'header' | 'query'; // Where to place the API key (default: 'header') } @@ -136,6 +119,7 @@ export interface ApiKeyAuth { * * For browser-based flows (implicit, authorization_code), obtain the token * separately and pass it as accessToken. + * Authorization URL: 'http://petstore.swagger.io/api/oauth/dialog' */ export interface OAuth2Auth { type: 'oauth2'; @@ -149,7 +133,7 @@ export interface OAuth2Auth { clientId?: string; /** Client secret (optional, depends on OAuth provider) */ clientSecret?: string; - /** Requested scopes */ + /** Requested scopes Available: write:pets, read:pets */ scopes?: string[]; /** Server-side flow type */ flow?: 'password' | 'client_credentials'; @@ -164,7 +148,7 @@ export interface OAuth2Auth { /** * Union type for all authentication methods - provides autocomplete support */ -export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth; +export type AuthConfig = ApiKeyAuth | OAuth2Auth; // ============================================================================ // Pagination Types diff --git a/test/codegen/generators/typescript/channels/protocols/http/fetch-security.spec.ts b/test/codegen/generators/typescript/channels/protocols/http/fetch-security.spec.ts new file mode 100644 index 00000000..89e29f68 --- /dev/null +++ b/test/codegen/generators/typescript/channels/protocols/http/fetch-security.spec.ts @@ -0,0 +1,230 @@ +import { + renderHttpCommonTypes, + ExtractedSecurityScheme +} from '../../../../../../../src/codegen/generators/typescript/channels/protocols/http/fetch'; + +describe('HTTP Fetch Generator - Security Types', () => { + describe('renderHttpCommonTypes with security schemes', () => { + it('should generate only apiKey auth type when only apiKey scheme defined', () => { + const securitySchemes: ExtractedSecurityScheme[] = [ + { + name: 'api_key', + type: 'apiKey', + apiKeyName: 'X-API-Key', + apiKeyIn: 'header' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + // Should include ApiKeyAuth interface + expect(result).toContain('export interface ApiKeyAuth'); + expect(result).toContain("type: 'apiKey'"); + + // Should include pre-populated values from the spec + expect(result).toContain('X-API-Key'); + expect(result).toContain('header'); + + // Should NOT include other auth types + expect(result).not.toContain('export interface BearerAuth'); + expect(result).not.toContain('export interface BasicAuth'); + expect(result).not.toContain('export interface OAuth2Auth'); + + // AuthConfig should only contain ApiKeyAuth + expect(result).toContain('export type AuthConfig = ApiKeyAuth'); + }); + + it('should generate only bearer auth type when http bearer scheme defined', () => { + const securitySchemes: ExtractedSecurityScheme[] = [ + { + name: 'bearerAuth', + type: 'http', + httpScheme: 'bearer' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface BearerAuth'); + expect(result).toContain("type: 'bearer'"); + expect(result).not.toContain('export interface ApiKeyAuth'); + expect(result).not.toContain('export interface BasicAuth'); + expect(result).not.toContain('export interface OAuth2Auth'); + expect(result).toContain('export type AuthConfig = BearerAuth'); + }); + + it('should generate only basic auth type when http basic scheme defined', () => { + const securitySchemes: ExtractedSecurityScheme[] = [ + { + name: 'basicAuth', + type: 'http', + httpScheme: 'basic' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface BasicAuth'); + expect(result).toContain("type: 'basic'"); + expect(result).not.toContain('export interface BearerAuth'); + expect(result).not.toContain('export interface ApiKeyAuth'); + expect(result).not.toContain('export interface OAuth2Auth'); + expect(result).toContain('export type AuthConfig = BasicAuth'); + }); + + it('should generate only oauth2 auth type when oauth2 scheme defined', () => { + const securitySchemes: ExtractedSecurityScheme[] = [ + { + name: 'oauth2', + type: 'oauth2', + oauth2Flows: { + clientCredentials: { + tokenUrl: 'https://example.com/token', + scopes: {read: 'read access'} + } + } + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface OAuth2Auth'); + expect(result).toContain("type: 'oauth2'"); + + // Should include pre-populated tokenUrl from the spec + expect(result).toContain('https://example.com/token'); + + expect(result).not.toContain('export interface BearerAuth'); + expect(result).not.toContain('export interface BasicAuth'); + expect(result).not.toContain('export interface ApiKeyAuth'); + expect(result).toContain('export type AuthConfig = OAuth2Auth'); + }); + + it('should generate multiple auth types when multiple schemes defined', () => { + const securitySchemes: ExtractedSecurityScheme[] = [ + { + name: 'api_key', + type: 'apiKey', + apiKeyName: 'api_key', + apiKeyIn: 'header' + }, + { + name: 'bearerAuth', + type: 'http', + httpScheme: 'bearer' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface ApiKeyAuth'); + expect(result).toContain('export interface BearerAuth'); + expect(result).not.toContain('export interface BasicAuth'); + expect(result).not.toContain('export interface OAuth2Auth'); + + // AuthConfig should be a union of the defined types + expect(result).toMatch(/export type AuthConfig = (?:ApiKeyAuth \| BearerAuth|BearerAuth \| ApiKeyAuth)/); + }); + + it('should generate all auth types when no security schemes defined (backward compatibility)', () => { + const result = renderHttpCommonTypes(); // No argument - backward compatible + + expect(result).toContain('export interface BearerAuth'); + expect(result).toContain('export interface BasicAuth'); + expect(result).toContain('export interface ApiKeyAuth'); + expect(result).toContain('export interface OAuth2Auth'); + expect(result).toContain( + 'export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth' + ); + }); + + it('should generate all auth types when empty security schemes array provided', () => { + const result = renderHttpCommonTypes([]); + + expect(result).toContain('export interface BearerAuth'); + expect(result).toContain('export interface BasicAuth'); + expect(result).toContain('export interface ApiKeyAuth'); + expect(result).toContain('export interface OAuth2Auth'); + }); + + it('should generate auth interface with pre-populated apiKey name and location', () => { + const securitySchemes: ExtractedSecurityScheme[] = [ + { + name: 'petstore_api_key', + type: 'apiKey', + apiKeyName: 'X-Petstore-Key', + apiKeyIn: 'query' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + // The generated interface should have name and in pre-set + expect(result).toContain('X-Petstore-Key'); + expect(result).toContain('query'); + }); + + it('should generate OAuth2 auth with tokenUrl from implicit flow', () => { + const securitySchemes: ExtractedSecurityScheme[] = [ + { + name: 'oauth2', + type: 'oauth2', + oauth2Flows: { + implicit: { + authorizationUrl: 'https://example.com/authorize', + scopes: {} + } + } + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface OAuth2Auth'); + expect(result).toContain('https://example.com/authorize'); + }); + + it('should handle openIdConnect type', () => { + const securitySchemes: ExtractedSecurityScheme[] = [ + { + name: 'oidc', + type: 'openIdConnect', + openIdConnectUrl: 'https://example.com/.well-known/openid-configuration' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + // OpenID Connect should be treated similar to OAuth2 + expect(result).toContain('export interface OAuth2Auth'); + expect(result).toContain('https://example.com/.well-known/openid-configuration'); + }); + + it('should deduplicate auth types when same type defined multiple times', () => { + const securitySchemes: ExtractedSecurityScheme[] = [ + { + name: 'api_key_header', + type: 'apiKey', + apiKeyName: 'X-API-Key', + apiKeyIn: 'header' + }, + { + name: 'api_key_query', + type: 'apiKey', + apiKeyName: 'api_key', + apiKeyIn: 'query' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + // Should only have one ApiKeyAuth interface + const apiKeyInterfaceMatches = result.match(/export interface ApiKeyAuth/g); + expect(apiKeyInterfaceMatches).toHaveLength(1); + + // AuthConfig should still only have ApiKeyAuth once + expect(result).toContain('export type AuthConfig = ApiKeyAuth'); + expect(result).not.toMatch(/ApiKeyAuth \| ApiKeyAuth/); + }); + }); +}); diff --git a/test/codegen/inputs/openapi/security.spec.ts b/test/codegen/inputs/openapi/security.spec.ts new file mode 100644 index 00000000..ab14108e --- /dev/null +++ b/test/codegen/inputs/openapi/security.spec.ts @@ -0,0 +1,645 @@ +import {OpenAPIV3, OpenAPIV2} from 'openapi-types'; +import { + extractSecuritySchemes, + getOperationSecurityRequirements, + ExtractedSecurityScheme +} from '../../../../src/codegen/inputs/openapi/security'; + +describe('OpenAPI Security Extraction', () => { + describe('extractSecuritySchemes', () => { + describe('OpenAPI 3.x', () => { + it('should extract apiKey security scheme from header', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + api_key: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'api_key', + type: 'apiKey', + apiKeyName: 'X-API-Key', + apiKeyIn: 'header' + }); + }); + + it('should extract apiKey security scheme from query', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + api_key: { + type: 'apiKey', + name: 'api_key', + in: 'query' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'api_key', + type: 'apiKey', + apiKeyName: 'api_key', + apiKeyIn: 'query' + }); + }); + + it('should extract apiKey security scheme from cookie', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + session: { + type: 'apiKey', + name: 'session_id', + in: 'cookie' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'session', + type: 'apiKey', + apiKeyName: 'session_id', + apiKeyIn: 'cookie' + }); + }); + + it('should extract http bearer security scheme', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'bearerAuth', + type: 'http', + httpScheme: 'bearer', + bearerFormat: 'JWT' + }); + }); + + it('should extract http basic security scheme', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + basicAuth: { + type: 'http', + scheme: 'basic' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'basicAuth', + type: 'http', + httpScheme: 'basic' + }); + }); + + it('should extract oauth2 implicit flow', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + oauth2: { + type: 'oauth2', + flows: { + implicit: { + authorizationUrl: 'https://example.com/oauth/authorize', + scopes: { + 'read:pets': 'read your pets', + 'write:pets': 'modify pets in your account' + } + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'oauth2', + type: 'oauth2', + oauth2Flows: { + implicit: { + authorizationUrl: 'https://example.com/oauth/authorize', + scopes: { + 'read:pets': 'read your pets', + 'write:pets': 'modify pets in your account' + } + } + } + }); + }); + + it('should extract oauth2 authorization code flow', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + oauth2: { + type: 'oauth2', + flows: { + authorizationCode: { + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: 'https://example.com/oauth/token', + scopes: { + 'read:users': 'read user data' + } + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.authorizationCode).toEqual({ + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: 'https://example.com/oauth/token', + scopes: { + 'read:users': 'read user data' + } + }); + }); + + it('should extract oauth2 client credentials flow', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + clientCredentials: { + type: 'oauth2', + flows: { + clientCredentials: { + tokenUrl: 'https://example.com/oauth/token', + scopes: { + admin: 'admin access' + } + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.clientCredentials).toEqual({ + tokenUrl: 'https://example.com/oauth/token', + scopes: { + admin: 'admin access' + } + }); + }); + + it('should extract oauth2 password flow', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + passwordAuth: { + type: 'oauth2', + flows: { + password: { + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.password).toEqual({ + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + }); + }); + + it('should extract openIdConnect security scheme', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + oidc: { + type: 'openIdConnect', + openIdConnectUrl: + 'https://example.com/.well-known/openid-configuration' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'oidc', + type: 'openIdConnect', + openIdConnectUrl: + 'https://example.com/.well-known/openid-configuration' + }); + }); + + it('should extract multiple security schemes', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + api_key: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header' + }, + bearerAuth: { + type: 'http', + scheme: 'bearer' + }, + oauth2: { + type: 'oauth2', + flows: { + implicit: { + authorizationUrl: 'https://example.com/oauth/authorize', + scopes: {} + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(3); + expect(schemes.map((s) => s.name)).toContain('api_key'); + expect(schemes.map((s) => s.name)).toContain('bearerAuth'); + expect(schemes.map((s) => s.name)).toContain('oauth2'); + }); + + it('should return empty array when no security schemes defined', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {} + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toEqual([]); + }); + + it('should return empty array when components is empty', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: {} + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toEqual([]); + }); + }); + + describe('Swagger 2.0 (OpenAPI 2.0)', () => { + it('should extract apiKey security definition from header', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + api_key: { + type: 'apiKey', + name: 'api_key', + in: 'header' + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'api_key', + type: 'apiKey', + apiKeyName: 'api_key', + apiKeyIn: 'header' + }); + }); + + it('should extract basic security definition', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + basicAuth: { + type: 'basic' + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'basicAuth', + type: 'http', + httpScheme: 'basic' + }); + }); + + it('should extract oauth2 implicit flow from Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + petstore_auth: { + type: 'oauth2', + flow: 'implicit', + authorizationUrl: 'https://petstore.swagger.io/oauth/authorize', + scopes: { + 'write:pets': 'modify pets', + 'read:pets': 'read pets' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'petstore_auth', + type: 'oauth2', + oauth2Flows: { + implicit: { + authorizationUrl: 'https://petstore.swagger.io/oauth/authorize', + scopes: { + 'write:pets': 'modify pets', + 'read:pets': 'read pets' + } + } + } + }); + }); + + it('should extract oauth2 password flow from Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + password_auth: { + type: 'oauth2', + flow: 'password', + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.password).toEqual({ + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + }); + }); + + it('should extract oauth2 application (client_credentials) flow from Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + app_auth: { + type: 'oauth2', + flow: 'application', + tokenUrl: 'https://example.com/oauth/token', + scopes: { + admin: 'admin access' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.clientCredentials).toEqual({ + tokenUrl: 'https://example.com/oauth/token', + scopes: { + admin: 'admin access' + } + }); + }); + + it('should extract oauth2 accessCode (authorization_code) flow from Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + code_auth: { + type: 'oauth2', + flow: 'accessCode', + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.authorizationCode).toEqual({ + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + }); + }); + + it('should return empty array when no securityDefinitions in Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {} + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toEqual([]); + }); + }); + }); + + describe('getOperationSecurityRequirements', () => { + it('should extract security requirements from operation', () => { + const operation: OpenAPIV3.OperationObject = { + responses: {}, + security: [ + { + bearerAuth: [] + } + ] + }; + + const requirements = getOperationSecurityRequirements(operation); + + expect(requirements).toEqual(['bearerAuth']); + }); + + it('should extract multiple security requirements', () => { + const operation: OpenAPIV3.OperationObject = { + responses: {}, + security: [ + { + bearerAuth: [], + api_key: [] + } + ] + }; + + const requirements = getOperationSecurityRequirements(operation); + + expect(requirements).toContain('bearerAuth'); + expect(requirements).toContain('api_key'); + }); + + it('should extract security requirements with scopes', () => { + const operation: OpenAPIV3.OperationObject = { + responses: {}, + security: [ + { + oauth2: ['read:pets', 'write:pets'] + } + ] + }; + + const requirements = getOperationSecurityRequirements(operation); + + expect(requirements).toEqual(['oauth2']); + }); + + it('should handle multiple security requirement objects (OR relationship)', () => { + const operation: OpenAPIV3.OperationObject = { + responses: {}, + security: [ + {bearerAuth: []}, + {api_key: []}, + {oauth2: ['read:pets']} + ] + }; + + const requirements = getOperationSecurityRequirements(operation); + + expect(requirements).toContain('bearerAuth'); + expect(requirements).toContain('api_key'); + expect(requirements).toContain('oauth2'); + }); + + it('should return empty array when no security defined', () => { + const operation: OpenAPIV3.OperationObject = { + responses: {} + }; + + const requirements = getOperationSecurityRequirements(operation); + + expect(requirements).toEqual([]); + }); + + it('should return empty array for empty security array', () => { + const operation: OpenAPIV3.OperationObject = { + responses: {}, + security: [] + }; + + const requirements = getOperationSecurityRequirements(operation); + + expect(requirements).toEqual([]); + }); + + it('should handle empty security object (no auth required for operation)', () => { + const operation: OpenAPIV3.OperationObject = { + responses: {}, + security: [{}] + }; + + const requirements = getOperationSecurityRequirements(operation); + + expect(requirements).toEqual([]); + }); + }); +}); diff --git a/test/runtime/typescript/src/openapi/channels/http_client.ts b/test/runtime/typescript/src/openapi/channels/http_client.ts index a552de90..cc989cd4 100644 --- a/test/runtime/typescript/src/openapi/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi/channels/http_client.ts @@ -97,30 +97,13 @@ export interface TokenResponse { // Security Configuration Types - Grouped for better autocomplete // ============================================================================ -/** - * Bearer token authentication configuration - */ -export interface BearerAuth { - type: 'bearer'; - token: string; -} - -/** - * Basic authentication configuration (username/password) - */ -export interface BasicAuth { - type: 'basic'; - username: string; - password: string; -} - /** * API key authentication configuration */ export interface ApiKeyAuth { type: 'apiKey'; key: string; - name?: string; // Name of the API key parameter (default: 'X-API-Key') + name?: string; // Name of the API key parameter (default: 'api_key') in?: 'header' | 'query'; // Where to place the API key (default: 'header') } @@ -134,6 +117,7 @@ export interface ApiKeyAuth { * * For browser-based flows (implicit, authorization_code), obtain the token * separately and pass it as accessToken. + * Authorization URL: 'http://petstore.swagger.io/api/oauth/dialog' */ export interface OAuth2Auth { type: 'oauth2'; @@ -147,7 +131,7 @@ export interface OAuth2Auth { clientId?: string; /** Client secret (optional, depends on OAuth provider) */ clientSecret?: string; - /** Requested scopes */ + /** Requested scopes Available: write:pets, read:pets */ scopes?: string[]; /** Server-side flow type */ flow?: 'password' | 'client_credentials'; @@ -162,7 +146,7 @@ export interface OAuth2Auth { /** * Union type for all authentication methods - provides autocomplete support */ -export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth; +export type AuthConfig = ApiKeyAuth | OAuth2Auth; // ============================================================================ // Pagination Types diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts new file mode 100644 index 00000000..77606c4a --- /dev/null +++ b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts @@ -0,0 +1,235 @@ +/* eslint-disable no-console */ +/** + * Runtime tests for dynamically generated security types from OpenAPI securitySchemes. + * + * These tests verify that: + * 1. Generated AuthConfig only contains auth types defined in the OpenAPI spec + * 2. The narrowed types work correctly at runtime + * 3. OAuth2 auth works with access tokens + * 4. The generated code compiles and works correctly at runtime + */ +import {createTestServer, runWithServer} from './test-utils'; +import {APet} from '../../../../src/openapi/payloads/APet'; +import { + postAddPet +} from '../../../../src/openapi/channels/http_client'; + +// Type-level test: Verify AuthConfig is narrowed to only defined types +// If this compiles, the types are correctly generated +type AssertAuthConfigHasOAuth2 = Extract< + import('../../../../src/openapi/channels/http_client').AuthConfig, + {type: 'oauth2'} +> extends never + ? 'missing' + : 'present'; + +type AssertAuthConfigHasApiKey = Extract< + import('../../../../src/openapi/channels/http_client').AuthConfig, + {type: 'apiKey'} +> extends never + ? 'missing' + : 'present'; + +// These should NOT exist if security schemes are properly extracted +type AssertAuthConfigMissingBasic = Extract< + import('../../../../src/openapi/channels/http_client').AuthConfig, + {type: 'basic'} +> extends never + ? 'correctly-missing' + : 'unexpectedly-present'; + +type AssertAuthConfigMissingBearer = Extract< + import('../../../../src/openapi/channels/http_client').AuthConfig, + {type: 'bearer'} +> extends never + ? 'correctly-missing' + : 'unexpectedly-present'; + +// Type assertions - these will fail at compile time if the types are wrong +const _assertOAuth2Present: AssertAuthConfigHasOAuth2 = 'present'; +const _assertApiKeyPresent: AssertAuthConfigHasApiKey = 'present'; +const _assertBasicMissing: AssertAuthConfigMissingBasic = 'correctly-missing'; +const _assertBearerMissing: AssertAuthConfigMissingBearer = 'correctly-missing'; + +jest.setTimeout(15000); + +describe('HTTP Client - Security Schemes from OpenAPI', () => { + describe('apiKey authentication (api_key from spec)', () => { + it('should allow apiKey auth type since it is in the spec', async () => { + const {app, router, port} = createTestServer(); + + const responsePet = new APet({ + id: 1, + name: 'Fluffy', + photoUrls: ['http://example.com/fluffy.jpg'] + }); + let receivedApiKey: string | undefined; + + // The spec defines: name: "api_key", in: "header" + // The generated interface has this as the default in the comment + router.post('/pet', (req, res) => { + receivedApiKey = req.headers['x-api-key'] as string; + res.setHeader('Content-Type', 'application/json'); + res.write(responsePet.marshal()); + res.end(); + }); + + return runWithServer(app, port, async () => { + const requestPet = new APet({ + name: 'Fluffy', + photoUrls: ['http://example.com/fluffy.jpg'] + }); + + // Use apiKey auth - users need to provide name/in but the defaults + // from the spec are documented in the generated interface + await postAddPet({ + payload: requestPet, + server: `http://localhost:${port}`, + auth: { + type: 'apiKey', + key: 'my-secret-api-key' + // Uses default header name 'X-API-Key' when not specified + } + }); + + expect(receivedApiKey).toBe('my-secret-api-key'); + }); + }); + + it('should allow specifying custom apiKey name from spec', async () => { + const {app, router, port} = createTestServer(); + + const responsePet = new APet({ + id: 1, + name: 'Fluffy', + photoUrls: [] + }); + let receivedApiKey: string | undefined; + + // The spec defines: name: "api_key", in: "header" + router.post('/pet', (req, res) => { + receivedApiKey = req.headers['api_key'] as string; + res.setHeader('Content-Type', 'application/json'); + res.write(responsePet.marshal()); + res.end(); + }); + + return runWithServer(app, port, async () => { + const requestPet = new APet({ + name: 'Fluffy', + photoUrls: [] + }); + + // Use the header name from the spec + await postAddPet({ + payload: requestPet, + server: `http://localhost:${port}`, + auth: { + type: 'apiKey', + key: 'my-secret-api-key', + name: 'api_key', // From spec: components.securitySchemes.api_key.name + in: 'header' // From spec: components.securitySchemes.api_key.in + } + }); + + expect(receivedApiKey).toBe('my-secret-api-key'); + }); + }); + }); + + describe('oauth2 authentication (petstore_auth from spec)', () => { + it('should allow oauth2 auth type since it is in the spec', async () => { + const {app, router, port} = createTestServer(); + + const responsePet = new APet({ + id: 1, + name: 'Fluffy', + photoUrls: ['http://example.com/fluffy.jpg'] + }); + + router.post('/pet', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.write(responsePet.marshal()); + res.end(); + }); + + return runWithServer(app, port, async () => { + const requestPet = new APet({ + name: 'Fluffy', + photoUrls: ['http://example.com/fluffy.jpg'] + }); + + // With a pre-obtained token, oauth2 works + const response = await postAddPet({ + payload: requestPet, + server: `http://localhost:${port}`, + auth: { + type: 'oauth2', + accessToken: 'pre-obtained-token' + } + }); + + expect(response.status).toBe(200); + }); + }); + + it('should accept scopes as the spec defines them', async () => { + const {app, router, port} = createTestServer(); + + const responsePet = new APet({ + id: 1, + name: 'Fluffy', + photoUrls: [] + }); + + router.post('/pet', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.write(responsePet.marshal()); + res.end(); + }); + + return runWithServer(app, port, async () => { + const requestPet = new APet({ + name: 'Test', + photoUrls: [] + }); + + // The spec defines scopes: write:pets, read:pets + // The generated interface documents these in the JSDoc + await postAddPet({ + payload: requestPet, + server: `http://localhost:${port}`, + auth: { + type: 'oauth2', + accessToken: 'token', + scopes: ['write:pets', 'read:pets'] + } + }); + }); + }); + }); + + describe('auth type narrowing', () => { + it('should reject invalid auth types at compile time', async () => { + // This test verifies at compile time that invalid auth types are rejected + // If the security schemes are properly extracted, 'basic' and 'bearer' + // should not be valid auth types for this OpenAPI spec + // + // The following commented code would cause a TypeScript error: + // + // await postAddPet({ + // payload: requestPet, + // server: `http://localhost:${port}`, + // auth: { type: 'basic', username: 'user', password: 'pass' } // TypeScript Error! + // }); + // + // await postAddPet({ + // payload: requestPet, + // server: `http://localhost:${port}`, + // auth: { type: 'bearer', token: 'token' } // TypeScript Error! + // }); + + expect(true).toBe(true); // Placeholder - the real test is at compile time + }); + }); +}); From 1a6014cafa33620b2dff9d32269f94f68196368f Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sun, 8 Mar 2026 22:18:42 +0100 Subject: [PATCH 2/9] fix: use dynamic port assignment in HTTP runtime tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HTTP runtime tests were using randomly generated ports which could cause EADDRINUSE errors when multiple tests ran in parallel and got the same random port. This fix: - Uses port 0 to let the OS assign an available port - Properly handles server errors with 'error' event listener - Passes the actual assigned port to test callbacks via a new parameter This ensures tests don't fail due to port collisions in CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../http_client/api_auth.spec.ts | 20 ++--- .../http_client/authentication.spec.ts | 32 ++++---- .../request_reply/http_client/basics.spec.ts | 28 +++---- .../request_reply/http_client/hooks.spec.ts | 52 ++++++------ .../request_reply/http_client/methods.spec.ts | 68 ++++++++-------- .../request_reply/http_client/oauth2.spec.ts | 80 +++++++++---------- .../oauth2_client_credentials.spec.ts | 18 ++--- .../http_client/oauth2_implicit_flow.spec.ts | 18 ++--- .../http_client/oauth2_password_flow.spec.ts | 18 ++--- .../http_client/oauth2_refresh_token.spec.ts | 24 +++--- .../request_reply/http_client/openapi.spec.ts | 20 ++--- .../http_client/pagination.spec.ts | 48 +++++------ .../http_client/parameters-headers.spec.ts | 42 +++++----- .../request_reply/http_client/retry.spec.ts | 48 +++++------ .../http_client/security_schemes.spec.ts | 20 ++--- .../request_reply/http_client/test-utils.ts | 30 ++++--- 16 files changed, 287 insertions(+), 279 deletions(-) diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts index c69cd964..43f99206 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts @@ -26,10 +26,10 @@ describe('HTTP Client - API Key and Basic Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: API_KEY, @@ -61,10 +61,10 @@ describe('HTTP Client - API Key and Basic Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: API_KEY, @@ -106,10 +106,10 @@ describe('HTTP Client - API Key and Basic Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'basic', username: USERNAME, @@ -146,10 +146,10 @@ describe('HTTP Client - API Key and Basic Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: BEARER_TOKEN @@ -172,11 +172,11 @@ describe('HTTP Client - API Key and Basic Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'wrong-api-key', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts index 65a55529..646ed2c3 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts @@ -23,11 +23,11 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'bearer', token: 'test-token-123' }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -50,11 +50,11 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'basic', username: 'user', password: 'pass' }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -78,11 +78,11 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'apiKey', key: 'my-api-key-123' }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -103,7 +103,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'apiKey', key: 'my-api-key-123', @@ -112,7 +112,7 @@ describe('HTTP Client - Authentication', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -133,7 +133,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'apiKey', key: 'custom-key-value', @@ -142,7 +142,7 @@ describe('HTTP Client - Authentication', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -165,9 +165,9 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'secret-key', @@ -199,14 +199,14 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'oauth2', accessToken: 'oauth-access-token-xyz' }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -233,9 +233,9 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: 'my-token' }, additionalHeaders: { 'X-Custom-Header': 'custom-value', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts index f6b876d6..900ab24a 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts @@ -24,10 +24,10 @@ describe('HTTP Client - Basics', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` }); expect(response.data).toBeDefined(); @@ -53,9 +53,9 @@ describe('HTTP Client - Basics', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination: { type: 'offset', offset: 0, limit: 20 } }); @@ -84,9 +84,9 @@ describe('HTTP Client - Basics', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, queryParams: { filter: 'active', sort: 'name' @@ -107,9 +107,9 @@ describe('HTTP Client - Basics', () => { res.status(401).json({ error: 'Unauthorized' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` })).rejects.toThrow('Unauthorized'); }); }); @@ -121,9 +121,9 @@ describe('HTTP Client - Basics', () => { res.status(403).json({ error: 'Forbidden' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` })).rejects.toThrow('Forbidden'); }); }); @@ -135,9 +135,9 @@ describe('HTTP Client - Basics', () => { res.status(404).json({ error: 'Not Found' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` })).rejects.toThrow('Not Found'); }); }); @@ -149,9 +149,9 @@ describe('HTTP Client - Basics', () => { res.status(500).json({ error: 'Internal Server Error' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` })).rejects.toThrow('Internal Server Error'); }); }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts index 1c020e98..60d094ae 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts @@ -24,7 +24,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => ({ ...params, @@ -36,7 +36,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); @@ -57,7 +57,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: async (params) => { await new Promise(resolve => setTimeout(resolve, 10)); @@ -72,7 +72,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); @@ -94,7 +94,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => ({ ...params, @@ -106,7 +106,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); @@ -127,7 +127,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => ({ ...params, @@ -136,7 +136,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); @@ -159,7 +159,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { afterResponse: (response, params) => { afterResponseCalled = true; @@ -169,7 +169,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); @@ -190,7 +190,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { let startTime: number; const hooks: HttpHooks = { @@ -209,7 +209,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); @@ -232,7 +232,7 @@ describe('HTTP Client - Hooks', () => { res.status(404).json({ error: 'Not Found' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { onError: (error, params) => { onErrorCalled = true; @@ -243,7 +243,7 @@ describe('HTTP Client - Hooks', () => { try { await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); } catch (error) { @@ -262,7 +262,7 @@ describe('HTTP Client - Hooks', () => { res.status(503).json({ error: 'Service Unavailable', retryAfter: 60 }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { onError: (error, params) => { const enhancedError = new Error(`Request to ${params.url} failed: ${error.message}`); @@ -271,7 +271,7 @@ describe('HTTP Client - Hooks', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks })).rejects.toThrow(/Request to.*failed/); }); @@ -286,7 +286,7 @@ describe('HTTP Client - Hooks', () => { res.status(500).json({ error: 'Server Error' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { onError: async (error, params) => { await new Promise(resolve => setTimeout(resolve, 10)); @@ -297,7 +297,7 @@ describe('HTTP Client - Hooks', () => { try { await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); } catch (error) { @@ -322,7 +322,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { makeRequest: async (params) => { customMakeRequestCalled = true; @@ -336,7 +336,7 @@ describe('HTTP Client - Hooks', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); @@ -359,7 +359,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => { hookCalls.push('beforeRequest'); @@ -381,7 +381,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks }); @@ -407,7 +407,7 @@ describe('HTTP Client - Hooks', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => { hookCalls.push('beforeRequest'); @@ -426,7 +426,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks, retry }); @@ -450,7 +450,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => { const url = new URL(params.url); @@ -463,7 +463,7 @@ describe('HTTP Client - Hooks', () => { }; const page1 = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, hooks, pagination: { type: 'offset', offset: 0, limit: 20 } }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts index 8edb5e35..dc5a2f87 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts @@ -29,9 +29,9 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` }); expect(receivedMethod).toBe('GET'); @@ -58,9 +58,9 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, payload: requestMessage }); @@ -89,9 +89,9 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putPingPutRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, payload: requestMessage }); @@ -116,9 +116,9 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await putPingPutRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, payload: requestMessage, auth: { type: 'bearer', token: 'put-token' } }); @@ -141,9 +141,9 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await putPingPutRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, payload: requestMessage, queryParams: { version: '2' } }); @@ -167,9 +167,9 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await deletePingDeleteRequest({ - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` }); expect(receivedMethod).toBe('DELETE'); @@ -191,9 +191,9 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await deletePingDeleteRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: 'delete-token' } }); @@ -219,9 +219,9 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await patchPingPatchRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, payload: requestMessage }); @@ -247,9 +247,9 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await patchPingPatchRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, payload: requestMessage, pagination: { type: 'offset', offset: 10, limit: 5 } }); @@ -273,12 +273,12 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // HEAD requests return no body, so the generated code may fail // This test verifies the method is sent correctly even if parsing fails try { await headPingHeadRequest({ - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` }); } catch (error) { // Expected - HEAD responses have no body to parse @@ -301,10 +301,10 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { try { await headPingHeadRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'head-key' } }); } catch (error) { @@ -330,11 +330,11 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // OPTIONS requests typically return no body try { await optionsPingOptionsRequest({ - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` }); } catch (error) { // Expected - OPTIONS responses typically have no body @@ -355,9 +355,9 @@ describe('HTTP Client - HTTP Methods', () => { res.status(404).json({ error: 'Not Found' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(putPingPutRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, payload: requestMessage })).rejects.toThrow('Not Found'); }); @@ -370,9 +370,9 @@ describe('HTTP Client - HTTP Methods', () => { res.status(500).json({ error: 'Internal Server Error' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(deletePingDeleteRequest({ - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` })).rejects.toThrow('Internal Server Error'); }); }); @@ -386,9 +386,9 @@ describe('HTTP Client - HTTP Methods', () => { res.status(403).json({ error: 'Forbidden' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(patchPingPatchRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, payload: requestMessage })).rejects.toThrow('Forbidden'); }); @@ -414,9 +414,9 @@ describe('HTTP Client - HTTP Methods', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putPingPutRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, payload: requestMessage, retry: { maxRetries: 3, initialDelayMs: 50, retryableStatusCodes: [503] } }); @@ -443,9 +443,9 @@ describe('HTTP Client - HTTP Methods', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await deletePingDeleteRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry: { maxRetries: 3, initialDelayMs: 50, retryableStatusCodes: [502] } }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts index 141c5a82..16b448e4 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts @@ -36,13 +36,13 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', clientId: 'test-client-id', clientSecret: 'test-client-secret', - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, scopes: ['read', 'write'], onTokenRefresh: (tokens) => { refreshedTokens.push(tokens); @@ -50,7 +50,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -68,7 +68,7 @@ describe('HTTP Client - OAuth2', () => { res.json({ error: 'should not reach here' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', @@ -76,7 +76,7 @@ describe('HTTP Client - OAuth2', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth })).rejects.toThrow('OAuth2 Client Credentials flow requires tokenUrl'); }); @@ -109,18 +109,18 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'password', clientId: 'test-client-id', - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, username: 'testuser', password: 'testpass' }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -138,16 +138,16 @@ describe('HTTP Client - OAuth2', () => { res.json({ error: 'should not reach here' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'password', clientId: 'test-client-id', - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` }; await expect(getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth })).rejects.toThrow('OAuth2 Password flow requires username'); }); @@ -184,17 +184,17 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', accessToken: 'expired-token', refreshToken: 'valid-refresh-token', clientId: 'test-client-id', - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -243,13 +243,13 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', clientId: 'test-client-id', clientSecret: 'test-client-secret', - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` }; const retry: RetryConfig = { @@ -262,7 +262,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth, retry }); @@ -307,12 +307,12 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'password', clientId: 'test-client-id', - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, username: 'user', password: 'pass' }; @@ -324,7 +324,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth, retry }); @@ -367,13 +367,13 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', accessToken: 'expired-token', refreshToken: 'valid-refresh-token', clientId: 'test-client-id', - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` }; const retry: RetryConfig = { @@ -383,7 +383,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth, retry }); @@ -422,13 +422,13 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', clientId: 'test-client-id', clientSecret: 'test-client-secret', - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` }; const retry: RetryConfig = { @@ -438,7 +438,7 @@ describe('HTTP Client - OAuth2', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth, retry })).rejects.toThrow(); @@ -474,13 +474,13 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', clientId: 'test-client-id', clientSecret: 'test-client-secret', - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` }; const retry: RetryConfig = { @@ -494,7 +494,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth, retry }); @@ -527,18 +527,18 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // Use 'implicit' flow which is not supported for token fetching const auth: OAuth2Auth = { type: 'oauth2', flow: 'implicit' as any, clientId: 'test-client', - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, accessToken: 'pre-existing-token' }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -562,7 +562,7 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // When no flow is specified but accessToken is provided, // it should be used directly without fetching const auth: OAuth2Auth = { @@ -571,7 +571,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -597,17 +597,17 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', clientId: 'test-client', - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, accessToken: 'already-have-token' // This should prevent token fetch }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); @@ -641,20 +641,20 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', accessToken: 'expired-token', refreshToken: 'valid-refresh', clientId: 'test-client', - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, onTokenRefresh: (tokens) => { refreshedTokens.push(tokens); } }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts index 586a0091..feae72fb 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts @@ -69,19 +69,19 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // Mock onTokenRefresh callback const onTokenRefresh = jest.fn(); const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'client_credentials', clientId: CLIENT_ID, clientSecret: CLIENT_SECRET, - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, onTokenRefresh } }); @@ -112,16 +112,16 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'client_credentials', clientId: CLIENT_ID, - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` } }); throw new Error('Expected request to fail with 401 status'); @@ -213,16 +213,16 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'client_credentials', clientId: CLIENT_ID, clientSecret: CLIENT_SECRET, - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` } }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts index b52a9ccf..4a836c8f 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts @@ -58,12 +58,12 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // This is how you'd use a token obtained from a browser-based flow // (implicit, authorization_code via PKCE, etc.) const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: ACCESS_TOKEN @@ -120,18 +120,18 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { res.status(401).json({ error: 'Invalid Token' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const onTokenRefresh = jest.fn(); // Use pre-obtained token with refresh capability const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: EXPIRED_ACCESS_TOKEN, refreshToken: REFRESH_TOKEN, - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, clientId: CLIENT_ID, onTokenRefresh } @@ -160,11 +160,11 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { res.status(401).json({ error: 'Unauthorized' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // Simplest OAuth2 usage - just pass the access token const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: ACCESS_TOKEN @@ -189,12 +189,12 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { res.json({ success: true }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { try { // OAuth2 config without access token and no server-side flow await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2' // No accessToken, no flow - should make request without auth header diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts index 1e4b4c50..623f1672 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts @@ -69,13 +69,13 @@ describe('HTTP Client - OAuth2 Password Flow', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // Mock onTokenRefresh callback const onTokenRefresh = jest.fn(); const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'password', @@ -83,7 +83,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { clientSecret: CLIENT_SECRET, username: USERNAME, password: PASSWORD, - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, onTokenRefresh } }); @@ -117,18 +117,18 @@ describe('HTTP Client - OAuth2 Password Flow', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'password', clientId: CLIENT_ID, username: INVALID_USERNAME, password: INVALID_PASSWORD, - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` } }); throw new Error('Expected request to fail with 401 status'); @@ -211,10 +211,10 @@ describe('HTTP Client - OAuth2 Password Flow', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'password', @@ -222,7 +222,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { username: USERNAME, password: PASSWORD, scopes: SCOPES, - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` } }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts index 7d6c78f4..bcd2a6c8 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts @@ -84,20 +84,20 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { res.status(401).json(TestResponses.unauthorized('Invalid Token').body); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // Mock onTokenRefresh callback const onTokenRefresh = jest.fn(); const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', clientId: CLIENT_ID, clientSecret: CLIENT_SECRET, accessToken: EXPIRED_ACCESS_TOKEN, refreshToken: REFRESH_TOKEN, - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, onTokenRefresh } }); @@ -138,17 +138,17 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { res.status(401).json(TestResponses.unauthorized('Token Expired').body); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', clientId: CLIENT_ID, accessToken: EXPIRED_ACCESS_TOKEN, refreshToken: INVALID_REFRESH_TOKEN, - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` } }); throw new Error('Expected request to fail with 401 status'); @@ -172,16 +172,16 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { res.status(401).json(TestResponses.unauthorized('Token Expired').body); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: EXPIRED_ACCESS_TOKEN, refreshToken: 'refresh-token', - tokenUrl: `http://localhost:${port}/oauth/token` + tokenUrl: `http://localhost:${actualPort}/oauth/token` } as any // Using any to bypass type checking }); throw new Error('Expected request to fail'); @@ -236,19 +236,19 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { // Mock onTokenRefresh callback const onTokenRefresh = jest.fn(); const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', clientId: CLIENT_ID, accessToken: EXPIRED_ACCESS_TOKEN, refreshToken: REFRESH_TOKEN, - tokenUrl: `http://localhost:${port}/oauth/token`, + tokenUrl: `http://localhost:${actualPort}/oauth/token`, onTokenRefresh } }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts index ea5227d5..fb3bb704 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts @@ -26,10 +26,10 @@ describe('HTTP Client - OpenAPI Generated', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postAddPet({ payload: requestPet, - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` }); expect(response.data).toBeDefined(); @@ -55,10 +55,10 @@ describe('HTTP Client - OpenAPI Generated', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdatePet({ payload: requestPet, - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` }); expect(receivedMethod).toBe('PUT'); @@ -82,7 +82,7 @@ describe('HTTP Client - OpenAPI Generated', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const params = new FindPetsByStatusAndCategoryParameters({ status: 'available', categoryId: 123 @@ -90,7 +90,7 @@ describe('HTTP Client - OpenAPI Generated', () => { const response = await getFindPetsByStatusAndCategory({ parameters: params, - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` }); expect(receivedPath).toContain('available'); @@ -108,11 +108,11 @@ describe('HTTP Client - OpenAPI Generated', () => { res.status(400).json({ error: 'Bad Request' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const pet = new APet({ name: 'Test', photoUrls: [] }); await expect(postAddPet({ payload: pet, - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` })).rejects.toThrow(); }); }); @@ -124,7 +124,7 @@ describe('HTTP Client - OpenAPI Generated', () => { res.status(404).json({ error: 'Not Found' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const params = new FindPetsByStatusAndCategoryParameters({ status: 'invalid', categoryId: 999 @@ -132,7 +132,7 @@ describe('HTTP Client - OpenAPI Generated', () => { await expect(getFindPetsByStatusAndCategory({ parameters: params, - server: `http://localhost:${port}` + server: `http://localhost:${actualPort}` })).rejects.toThrow('Not Found'); }); }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts index ac8ed95a..ee74bc9f 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts @@ -25,7 +25,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'offset', offset: 20, @@ -33,7 +33,7 @@ describe('HTTP Client - Pagination', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination }); @@ -57,7 +57,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'offset', in: 'header', @@ -66,7 +66,7 @@ describe('HTTP Client - Pagination', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination }); @@ -90,7 +90,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'offset', offset: 100, @@ -100,7 +100,7 @@ describe('HTTP Client - Pagination', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination }); @@ -126,7 +126,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'cursor', cursor: 'abc123xyz', @@ -134,7 +134,7 @@ describe('HTTP Client - Pagination', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination }); @@ -165,9 +165,9 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const page1 = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination: { type: 'cursor', limit: 10 } }); @@ -202,7 +202,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'page', page: 3, @@ -210,7 +210,7 @@ describe('HTTP Client - Pagination', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination }); @@ -234,7 +234,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'range', start: 0, @@ -243,7 +243,7 @@ describe('HTTP Client - Pagination', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination }); @@ -265,9 +265,9 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const page1 = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination: { type: 'range', start: 0, end: 24, unit: 'items' } }); @@ -293,9 +293,9 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination: { type: 'offset', offset: 0, limit: 20 } }); @@ -324,9 +324,9 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const page1 = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination: { type: 'offset', offset: 0, limit: 20 } }); @@ -357,9 +357,9 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const page = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination: { type: 'offset', offset: 60, limit: 20 } }); @@ -383,9 +383,9 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, pagination: { type: 'page', page: 1, pageSize: 20 } }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts index 23da52e2..57ccf4a7 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts @@ -27,14 +27,14 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const parameters = new UserItemsParameters({ userId: 'user-123', itemId: '456' }); const response = await getGetUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters }); @@ -61,14 +61,14 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await getGetUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'alice', itemId: '100' }) }); await getGetUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'bob', itemId: '200' }) }); @@ -96,9 +96,9 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getGetUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'secure-user', itemId: '999' }), auth: { type: 'bearer', token: 'secret-token' } }); @@ -128,9 +128,9 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await getGetUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'user1', itemId: '42' }), queryParams: { include: 'metadata', @@ -164,7 +164,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const headers = new ItemRequestHeaders({ xCorrelationId: 'corr-123-abc', xRequestId: 'req-456-def' @@ -177,7 +177,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); const response = await putUpdateUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'user-1', itemId: '100' }), payload, requestHeaders: headers @@ -206,7 +206,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const headers = new ItemRequestHeaders({ xCorrelationId: 'required-only' }); @@ -216,7 +216,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); const response = await putUpdateUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u1', itemId: '1' }), payload, requestHeaders: headers @@ -245,9 +245,9 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdateUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u', itemId: '1' }), payload: new ItemRequest({ name: 'Item' }), requestHeaders: new ItemRequestHeaders({ xCorrelationId: 'corr-id' }), @@ -279,9 +279,9 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { await putUpdateUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u', itemId: '1' }), payload: new ItemRequest({ name: 'Secure Item' }), requestHeaders: new ItemRequestHeaders({ xCorrelationId: 'secure-corr' }), @@ -326,9 +326,9 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdateUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'full-user', itemId: '999' }), payload: new ItemRequest({ name: 'Complete Item', @@ -364,7 +364,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const parameters = new UserItemsParameters({ userId: 'user-1', itemId: 'non-existent' @@ -372,7 +372,7 @@ describe('HTTP Client - Parameters and Headers', () => { try { const response = await getGetUserItem({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, parameters }); expect(response.status).toBe(404); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts index 23383fa8..586a07c3 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts @@ -27,7 +27,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 100, @@ -35,7 +35,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry }); @@ -62,7 +62,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 5, initialDelayMs: 30, @@ -73,7 +73,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry }); @@ -103,7 +103,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50, @@ -114,7 +114,7 @@ describe('HTTP Client - Retry Logic', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry }); @@ -135,7 +135,7 @@ describe('HTTP Client - Retry Logic', () => { res.status(500).json({ error: 'Server Error' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50, @@ -143,7 +143,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry })).rejects.toThrow('Internal Server Error'); @@ -171,7 +171,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 5, initialDelayMs: 100, @@ -183,7 +183,7 @@ describe('HTTP Client - Retry Logic', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry }); @@ -212,7 +212,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 5, initialDelayMs: 100, @@ -225,7 +225,7 @@ describe('HTTP Client - Retry Logic', () => { }; await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry }); @@ -246,7 +246,7 @@ describe('HTTP Client - Retry Logic', () => { res.status(400).json({ error: 'Bad Request' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50, @@ -254,7 +254,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry })).rejects.toThrow(); @@ -279,14 +279,14 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50 }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry }); @@ -305,7 +305,7 @@ describe('HTTP Client - Retry Logic', () => { res.status(500).json({ error: 'Server Error' }); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50, @@ -313,7 +313,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry })).rejects.toThrow('Internal Server Error'); @@ -338,14 +338,14 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50 }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry }); @@ -371,14 +371,14 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50 }; const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry }); @@ -432,7 +432,7 @@ describe('HTTP Client - Retry Logic', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50 @@ -440,7 +440,7 @@ describe('HTTP Client - Retry Logic', () => { // Request should succeed on first try const response = await getPingGetRequest({ - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, retry }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts index 77606c4a..ea6d5576 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts @@ -74,7 +74,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const requestPet = new APet({ name: 'Fluffy', photoUrls: ['http://example.com/fluffy.jpg'] @@ -84,7 +84,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // from the spec are documented in the generated interface await postAddPet({ payload: requestPet, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'my-secret-api-key' @@ -114,7 +114,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const requestPet = new APet({ name: 'Fluffy', photoUrls: [] @@ -123,7 +123,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // Use the header name from the spec await postAddPet({ payload: requestPet, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'my-secret-api-key', @@ -153,7 +153,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const requestPet = new APet({ name: 'Fluffy', photoUrls: ['http://example.com/fluffy.jpg'] @@ -162,7 +162,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // With a pre-obtained token, oauth2 works const response = await postAddPet({ payload: requestPet, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: 'pre-obtained-token' @@ -188,7 +188,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { res.end(); }); - return runWithServer(app, port, async () => { + return runWithServer(app, port, async (_server, actualPort) => { const requestPet = new APet({ name: 'Test', photoUrls: [] @@ -198,7 +198,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // The generated interface documents these in the JSDoc await postAddPet({ payload: requestPet, - server: `http://localhost:${port}`, + server: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: 'token', @@ -219,13 +219,13 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // // await postAddPet({ // payload: requestPet, - // server: `http://localhost:${port}`, + // server: `http://localhost:${actualPort}`, // auth: { type: 'basic', username: 'user', password: 'pass' } // TypeScript Error! // }); // // await postAddPet({ // payload: requestPet, - // server: `http://localhost:${port}`, + // server: `http://localhost:${actualPort}`, // auth: { type: 'bearer', token: 'token' } // TypeScript Error! // }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/test-utils.ts b/test/runtime/typescript/test/channels/request_reply/http_client/test-utils.ts index 0064a3cf..0eafab24 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/test-utils.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/test-utils.ts @@ -1,6 +1,6 @@ import express, { Router, Express } from 'express'; import bodyParser from 'body-parser'; -import { Server } from 'http'; +import { Server, AddressInfo } from 'http'; /** * Helper function to create an Express server for HTTP client tests @@ -16,26 +16,34 @@ export function createTestServer(): { app.use(express.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true })); app.use(router); - - // Generate a random port between 5779 and 9875 - const port = Math.floor(Math.random() * (9875 - 5779 + 1)) + 5779; - - return { app, router, port }; + + // Return a placeholder port - actual port will be assigned dynamically + return { app, router, port: 0 }; } /** * Start an Express server and run the test function - * This handles proper server cleanup after the test + * This handles proper server cleanup after the test. + * Uses port 0 to let the OS assign an available port, avoiding EADDRINUSE errors. */ export function runWithServer( server: Express, - port: number, - testFn: (server: Server) => Promise + _port: number, + testFn: (server: Server, port: number) => Promise ): Promise { return new Promise((resolve, reject) => { - const httpServer = server.listen(port, async () => { + // Use port 0 to let the OS assign an available port + const httpServer = server.listen(0); + + httpServer.on('error', (error) => { + reject(error); + }); + + httpServer.on('listening', async () => { + const address = httpServer.address() as AddressInfo; + const assignedPort = address.port; try { - await testFn(httpServer); + await testFn(httpServer, assignedPort); resolve(); } catch (error) { reject(error); From dec8c7ba0da6ad30dc4b5cbf27088ee1507a11f9 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Mon, 9 Mar 2026 08:54:51 +0100 Subject: [PATCH 3/9] fix: ensure OAuth2 helper functions are always available in generated code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added renderOAuth2Stubs() to generate type-safe stub functions when OAuth2 is not needed, ensuring TypeScript compilation succeeds - Changed fallback AuthConfig to use 'never' type instead of union of all auth types when no recognized security schemes exist - Added AUTH_FEATURES.oauth2 runtime guards to generated function code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../channels/protocols/http/fetch.ts | 358 ++++++++++-------- 1 file changed, 204 insertions(+), 154 deletions(-) diff --git a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts index 94c56e39..c12582a5 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts @@ -316,9 +316,10 @@ function renderAuthConfigType(requirements: AuthTypeRequirements): string { types.push('OAuth2Auth'); } - // If no types, default to all (shouldn't happen but be safe) + // If no types are needed (e.g., no recognized security schemes), don't generate AuthConfig + // The auth field in HttpClientContext is optional, so this is safe if (types.length === 0) { - return 'export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth;'; + return '// No authentication types needed for this API\nexport type AuthConfig = never;'; } return `/** @@ -369,6 +370,194 @@ function renderSecurityTypes( return parts.join('\n'); } +/** + * Generate OAuth2 stub functions when OAuth2 is not available. + * These stubs ensure TypeScript compilation succeeds when generated code + * references OAuth2 functions, but the runtime guards prevent them from being called. + */ +function renderOAuth2Stubs(): string { + const code = [ + '', + '// OAuth2 helpers not needed for this API - provide type-safe stubs', + '// These are never called due to AUTH_FEATURES.oauth2 runtime guards', + 'type OAuth2Auth = never;', + 'function validateOAuth2Config(_auth: OAuth2Auth): void {}', + 'async function handleOAuth2TokenFlow(', + ' _auth: OAuth2Auth,', + ' _originalParams: HttpRequestParams,', + ' _makeRequest: (params: HttpRequestParams) => Promise,', + ' _retryConfig?: RetryConfig', + '): Promise { return null; }', + 'async function handleTokenRefresh(', + ' _auth: OAuth2Auth,', + ' _originalParams: HttpRequestParams,', + ' _makeRequest: (params: HttpRequestParams) => Promise,', + ' _retryConfig?: RetryConfig', + '): Promise { return null; }' + ]; + + return code.join('\n'); +} + +/** + * Generates OAuth2-specific helper functions. + * Only included when OAuth2 auth is needed. + */ +function renderOAuth2Helpers(): string { + const code = [ + '', + '/**', + ' * Validate OAuth2 configuration based on flow type', + ' */', + 'function validateOAuth2Config(auth: OAuth2Auth): void {', + ' // If using a flow, validate required fields', + ' switch (auth.flow) {', + ' case \'client_credentials\':', + ' if (!auth.tokenUrl) throw new Error(\'OAuth2 Client Credentials flow requires tokenUrl\');', + ' if (!auth.clientId) throw new Error(\'OAuth2 Client Credentials flow requires clientId\');', + ' break;', + '', + ' case \'password\':', + ' if (!auth.tokenUrl) throw new Error(\'OAuth2 Password flow requires tokenUrl\');', + ' if (!auth.clientId) throw new Error(\'OAuth2 Password flow requires clientId\');', + ' if (!auth.username) throw new Error(\'OAuth2 Password flow requires username\');', + ' if (!auth.password) throw new Error(\'OAuth2 Password flow requires password\');', + ' break;', + '', + ' default:', + ' // No flow specified - must have accessToken for OAuth2 to work', + ' if (!auth.accessToken && !auth.flow) {', + ' // This is fine - token refresh can still work if refreshToken is provided', + ' // Or the request will just be made without auth', + ' }', + ' break;', + ' }', + '}', + '', + '/**', + ' * Handle OAuth2 token flows (client_credentials, password)', + ' */', + 'async function handleOAuth2TokenFlow(', + ' auth: OAuth2Auth,', + ' originalParams: HttpRequestParams,', + ' makeRequest: (params: HttpRequestParams) => Promise,', + ' retryConfig?: RetryConfig', + '): Promise {', + ' if (!auth.flow || !auth.tokenUrl) return null;', + '', + ' const params = new URLSearchParams();', + '', + ' if (auth.flow === \'client_credentials\') {', + ' params.append(\'grant_type\', \'client_credentials\');', + ' params.append(\'client_id\', auth.clientId!);', + ' } else if (auth.flow === \'password\') {', + ' params.append(\'grant_type\', \'password\');', + ' params.append(\'username\', auth.username || \'\');', + ' params.append(\'password\', auth.password || \'\');', + ' params.append(\'client_id\', auth.clientId!);', + ' } else {', + ' return null;', + ' }', + '', + ' if (auth.clientSecret) {', + ' params.append(\'client_secret\', auth.clientSecret);', + ' }', + ' if (auth.scopes && auth.scopes.length > 0) {', + ' params.append(\'scope\', auth.scopes.join(\' \'));', + ' }', + '', + ' const authHeaders: Record = {', + ' \'Content-Type\': \'application/x-www-form-urlencoded\'', + ' };', + '', + ` // Use basic auth for client credentials if both client ID and secret are provided`, + ` if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) {`, + ` const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64');`, + ` authHeaders['Authorization'] = \`Basic \${credentials}\`;`, + ` params.delete('client_id');`, + ` params.delete('client_secret');`, + ` }`, + ``, + ` const tokenResponse = await NodeFetch.default(auth.tokenUrl, {`, + ` method: 'POST',`, + ` headers: authHeaders,`, + ` body: params.toString()`, + ` });`, + ``, + ` if (!tokenResponse.ok) {`, + ` throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`);`, + ` }`, + '', + ' const tokenData = await tokenResponse.json();', + ' const tokens: TokenResponse = {', + ' accessToken: tokenData.access_token,', + ' refreshToken: tokenData.refresh_token,', + ' expiresIn: tokenData.expires_in', + ' };', + '', + ' // Notify the client about the tokens', + ' if (auth.onTokenRefresh) {', + ' auth.onTokenRefresh(tokens);', + ' }', + '', + ` // Retry the original request with the new token`, + ` const updatedHeaders = { ...originalParams.headers };`, + ` updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`;`, + ``, + ` return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig);`, + `}`, + '', + '/**', + ' * Handle OAuth2 token refresh on 401 response', + ' */', + 'async function handleTokenRefresh(', + ' auth: OAuth2Auth,', + ' originalParams: HttpRequestParams,', + ' makeRequest: (params: HttpRequestParams) => Promise,', + ' retryConfig?: RetryConfig', + '): Promise {', + ' if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null;', + '', + ' const refreshResponse = await NodeFetch.default(auth.tokenUrl, {', + ' method: \'POST\',', + ' headers: {', + ' \'Content-Type\': \'application/x-www-form-urlencoded\'', + ' },', + ' body: new URLSearchParams({', + ' grant_type: \'refresh_token\',', + ' refresh_token: auth.refreshToken,', + ' client_id: auth.clientId,', + ' ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {})', + ' }).toString()', + ' });', + '', + ' if (!refreshResponse.ok) {', + ' throw new Error(\'Unauthorized\');', + ' }', + '', + ' const tokenData = await refreshResponse.json();', + ' const newTokens: TokenResponse = {', + ' accessToken: tokenData.access_token,', + ' refreshToken: tokenData.refresh_token || auth.refreshToken,', + ' expiresIn: tokenData.expires_in', + ' };', + '', + ' // Notify the client about the refreshed tokens', + ' if (auth.onTokenRefresh) {', + ' auth.onTokenRefresh(newTokens);', + ' }', + '', + ` // Retry the original request with the new token`, + ` const updatedHeaders = { ...originalParams.headers };`, + ` updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`;`, + ``, + ` return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig);`, + `}` + ]; + + return code.join('\n'); +} + /** * Generates common types and helper functions shared across all HTTP client functions. * This should be called once per protocol generation to avoid code duplication. @@ -380,6 +569,7 @@ function renderSecurityTypes( export function renderHttpCommonTypes( securitySchemes?: ExtractedSecurityScheme[] ): string { + const requirements = analyzeSecuritySchemes(securitySchemes); const securityTypes = renderSecurityTypes(securitySchemes); return `// ============================================================================ @@ -465,6 +655,14 @@ export interface TokenResponse { ${securityTypes} +/** + * Feature flags indicating which auth types are available. + * Used internally to conditionally call auth-specific helpers. + */ +const AUTH_FEATURES = { + oauth2: ${requirements.oauth2} +} as const; + // ============================================================================ // Pagination Types // ============================================================================ @@ -682,34 +880,6 @@ function applyAuth( return { headers, url }; } -/** - * Validate OAuth2 configuration based on flow type - */ -function validateOAuth2Config(auth: OAuth2Auth): void { - // If using a flow, validate required fields - switch (auth.flow) { - case 'client_credentials': - if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); - break; - - case 'password': - if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); - if (!auth.username) throw new Error('OAuth2 Password flow requires username'); - if (!auth.password) throw new Error('OAuth2 Password flow requires password'); - break; - - default: - // No flow specified - must have accessToken for OAuth2 to work - if (!auth.accessToken && !auth.flow) { - // This is fine - token refresh can still work if refreshToken is provided - // Or the request will just be made without auth - } - break; - } -} - /** * Apply pagination parameters to URL and/or headers based on configuration */ @@ -899,126 +1069,6 @@ async function executeWithRetry( throw lastError ?? new Error('Request failed after retries'); } -/** - * Handle OAuth2 token flows (client_credentials, password) - */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); - authHeaders['Authorization'] = \`Basic \${credentials}\`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -1290,7 +1340,7 @@ function applyTypedHeaders( return headers; } - +${requirements.oauth2 ? renderOAuth2Helpers() : renderOAuth2Stubs()} // ============================================================================ // Generated HTTP Client Functions // ============================================================================`; @@ -1466,7 +1516,7 @@ function generateFunctionImplementation(params: { }; // Validate OAuth2 config if present - if (config.auth?.type === 'oauth2') { + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { validateOAuth2Config(config.auth); } @@ -1516,7 +1566,7 @@ function generateFunctionImplementation(params: { } // Handle OAuth2 token flows that require getting a token first - if (config.auth?.type === 'oauth2' && !config.auth.accessToken) { + if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); if (tokenFlowResponse) { response = tokenFlowResponse; @@ -1524,7 +1574,7 @@ function generateFunctionImplementation(params: { } // Handle 401 with token refresh - if (response.status === 401 && config.auth?.type === 'oauth2') { + if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); if (refreshResponse) { From bdba1cb034c903ed4fd0c7d558467afb539f6965 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Mon, 9 Mar 2026 08:56:05 +0100 Subject: [PATCH 4/9] wip --- .../generators/typescript/channels/types.ts | 6 - src/codegen/inputs/openapi/security.ts | 27 -- test/codegen/inputs/openapi/security.spec.ts | 99 ----- .../src/openapi/channels/http_client.ts | 321 ++++++++-------- .../src/request-reply/channels/http_client.ts | 363 +++++++++--------- 5 files changed, 349 insertions(+), 467 deletions(-) diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index 3206440c..d30fd6b6 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -251,12 +251,6 @@ export interface RenderHttpParameters { * When true, use unmarshalByStatusCode(json, statusCode) instead of unmarshal(json). */ includesStatusCodes?: boolean; - /** - * Security schemes extracted from the OpenAPI document. - * When provided, only auth types for these schemes will be generated. - * When undefined or empty, all auth types are generated for backward compatibility. - */ - securitySchemes?: ExtractedSecurityScheme[]; } export type SupportedProtocols = diff --git a/src/codegen/inputs/openapi/security.ts b/src/codegen/inputs/openapi/security.ts index f2fcfc77..0a40aa46 100644 --- a/src/codegen/inputs/openapi/security.ts +++ b/src/codegen/inputs/openapi/security.ts @@ -53,10 +53,6 @@ type OpenAPIDocument = | OpenAPIV3.Document | OpenAPIV2.Document | OpenAPIV3_1.Document; -type OpenAPIOperation = - | OpenAPIV3.OperationObject - | OpenAPIV2.OperationObject - | OpenAPIV3_1.OperationObject; /** * Extracts security schemes from an OpenAPI document. @@ -297,26 +293,3 @@ function extractSwagger2OAuth2Flow( return result; } - -/** - * Extracts security requirement names from an OpenAPI operation. - * Returns the unique security scheme names that the operation requires. - */ -export function getOperationSecurityRequirements( - operation: OpenAPIOperation -): string[] { - const security = operation.security; - if (!security || security.length === 0) { - return []; - } - - const requirements = new Set(); - - for (const requirement of security) { - for (const schemeName of Object.keys(requirement)) { - requirements.add(schemeName); - } - } - - return Array.from(requirements); -} diff --git a/test/codegen/inputs/openapi/security.spec.ts b/test/codegen/inputs/openapi/security.spec.ts index ab14108e..46a5963b 100644 --- a/test/codegen/inputs/openapi/security.spec.ts +++ b/test/codegen/inputs/openapi/security.spec.ts @@ -1,7 +1,6 @@ import {OpenAPIV3, OpenAPIV2} from 'openapi-types'; import { extractSecuritySchemes, - getOperationSecurityRequirements, ExtractedSecurityScheme } from '../../../../src/codegen/inputs/openapi/security'; @@ -544,102 +543,4 @@ describe('OpenAPI Security Extraction', () => { }); }); }); - - describe('getOperationSecurityRequirements', () => { - it('should extract security requirements from operation', () => { - const operation: OpenAPIV3.OperationObject = { - responses: {}, - security: [ - { - bearerAuth: [] - } - ] - }; - - const requirements = getOperationSecurityRequirements(operation); - - expect(requirements).toEqual(['bearerAuth']); - }); - - it('should extract multiple security requirements', () => { - const operation: OpenAPIV3.OperationObject = { - responses: {}, - security: [ - { - bearerAuth: [], - api_key: [] - } - ] - }; - - const requirements = getOperationSecurityRequirements(operation); - - expect(requirements).toContain('bearerAuth'); - expect(requirements).toContain('api_key'); - }); - - it('should extract security requirements with scopes', () => { - const operation: OpenAPIV3.OperationObject = { - responses: {}, - security: [ - { - oauth2: ['read:pets', 'write:pets'] - } - ] - }; - - const requirements = getOperationSecurityRequirements(operation); - - expect(requirements).toEqual(['oauth2']); - }); - - it('should handle multiple security requirement objects (OR relationship)', () => { - const operation: OpenAPIV3.OperationObject = { - responses: {}, - security: [ - {bearerAuth: []}, - {api_key: []}, - {oauth2: ['read:pets']} - ] - }; - - const requirements = getOperationSecurityRequirements(operation); - - expect(requirements).toContain('bearerAuth'); - expect(requirements).toContain('api_key'); - expect(requirements).toContain('oauth2'); - }); - - it('should return empty array when no security defined', () => { - const operation: OpenAPIV3.OperationObject = { - responses: {} - }; - - const requirements = getOperationSecurityRequirements(operation); - - expect(requirements).toEqual([]); - }); - - it('should return empty array for empty security array', () => { - const operation: OpenAPIV3.OperationObject = { - responses: {}, - security: [] - }; - - const requirements = getOperationSecurityRequirements(operation); - - expect(requirements).toEqual([]); - }); - - it('should handle empty security object (no auth required for operation)', () => { - const operation: OpenAPIV3.OperationObject = { - responses: {}, - security: [{}] - }; - - const requirements = getOperationSecurityRequirements(operation); - - expect(requirements).toEqual([]); - }); - }); }); diff --git a/test/runtime/typescript/src/openapi/channels/http_client.ts b/test/runtime/typescript/src/openapi/channels/http_client.ts index cc989cd4..fa3d0ed3 100644 --- a/test/runtime/typescript/src/openapi/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi/channels/http_client.ts @@ -148,6 +148,14 @@ export interface OAuth2Auth { */ export type AuthConfig = ApiKeyAuth | OAuth2Auth; +/** + * Feature flags indicating which auth types are available. + * Used internally to conditionally call auth-specific helpers. + */ +const AUTH_FEATURES = { + oauth2: true +} as const; + // ============================================================================ // Pagination Types // ============================================================================ @@ -365,34 +373,6 @@ function applyAuth( return { headers, url }; } -/** - * Validate OAuth2 configuration based on flow type - */ -function validateOAuth2Config(auth: OAuth2Auth): void { - // If using a flow, validate required fields - switch (auth.flow) { - case 'client_credentials': - if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); - break; - - case 'password': - if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); - if (!auth.username) throw new Error('OAuth2 Password flow requires username'); - if (!auth.password) throw new Error('OAuth2 Password flow requires password'); - break; - - default: - // No flow specified - must have accessToken for OAuth2 to work - if (!auth.accessToken && !auth.flow) { - // This is fine - token refresh can still work if refreshToken is provided - // Or the request will just be made without auth - } - break; - } -} - /** * Apply pagination parameters to URL and/or headers based on configuration */ @@ -582,126 +562,6 @@ async function executeWithRetry( throw lastError ?? new Error('Request failed after retries'); } -/** - * Handle OAuth2 token flows (client_credentials, password) - */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64'); - authHeaders['Authorization'] = `Basic ${credentials}`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -974,6 +834,153 @@ function applyTypedHeaders( return headers; } +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; + + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; + + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; + } +} + +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; + + const params = new URLSearchParams(); + + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; + } + + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } + + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} // ============================================================================ // Generated HTTP Client Functions // ============================================================================ @@ -992,7 +999,7 @@ async function postAddPet(context: PostAddPetContext): Promise Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64'); - authHeaders['Authorization'] = `Basic ${credentials}`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -991,6 +851,153 @@ function applyTypedHeaders( return headers; } +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; + + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; + + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; + } +} + +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; + + const params = new URLSearchParams(); + + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; + } + + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } + + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} // ============================================================================ // Generated HTTP Client Functions // ============================================================================ @@ -1009,7 +1016,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise }; // Validate OAuth2 config if present - if (config.auth?.type === 'oauth2') { + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { validateOAuth2Config(config.auth); } @@ -1061,7 +1068,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise } // Handle OAuth2 token flows that require getting a token first - if (config.auth?.type === 'oauth2' && !config.auth.accessToken) { + if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); if (tokenFlowResponse) { response = tokenFlowResponse; @@ -1069,7 +1076,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise } // Handle 401 with token refresh - if (response.status === 401 && config.auth?.type === 'oauth2') { + if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); if (refreshResponse) { @@ -1128,7 +1135,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis }; // Validate OAuth2 config if present - if (config.auth?.type === 'oauth2') { + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { validateOAuth2Config(config.auth); } @@ -1180,7 +1187,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis } // Handle OAuth2 token flows that require getting a token first - if (config.auth?.type === 'oauth2' && !config.auth.accessToken) { + if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); if (tokenFlowResponse) { response = tokenFlowResponse; @@ -1188,7 +1195,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis } // Handle 401 with token refresh - if (response.status === 401 && config.auth?.type === 'oauth2') { + if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); if (refreshResponse) { @@ -1248,7 +1255,7 @@ async function putPingPutRequest(context: PutPingPutRequestContext): Promise Date: Mon, 9 Mar 2026 09:37:33 +0100 Subject: [PATCH 5/9] fix: address PR review comments for HTTP security types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix ApiKeyAuth default mismatch: generated interface docs and runtime now use consistent spec-derived defaults instead of hardcoded 'X-API-Key' - Eliminate redundant analyzeSecuritySchemes call by passing pre-computed requirements to renderSecurityTypes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../channels/protocols/http/fetch.ts | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts index c12582a5..a5a27892 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts @@ -332,9 +332,10 @@ export type AuthConfig = ${types.join(' | ')};`; * Generates the security configuration types based on extracted security schemes. */ function renderSecurityTypes( - schemes: ExtractedSecurityScheme[] | undefined + schemes: ExtractedSecurityScheme[] | undefined, + requirements?: AuthTypeRequirements ): string { - const requirements = analyzeSecuritySchemes(schemes); + const authRequirements = requirements ?? analyzeSecuritySchemes(schemes); const parts: string[] = [ '// ============================================================================', @@ -344,28 +345,28 @@ function renderSecurityTypes( ]; // Only generate interfaces for required auth types - if (requirements.bearer) { + if (authRequirements.bearer) { parts.push(renderBearerAuthInterface()); parts.push(''); } - if (requirements.basic) { + if (authRequirements.basic) { parts.push(renderBasicAuthInterface()); parts.push(''); } - if (requirements.apiKey) { - parts.push(renderApiKeyAuthInterface(requirements.apiKeySchemes)); + if (authRequirements.apiKey) { + parts.push(renderApiKeyAuthInterface(authRequirements.apiKeySchemes)); parts.push(''); } - if (requirements.oauth2) { - parts.push(renderOAuth2AuthInterface(requirements.oauth2Schemes)); + if (authRequirements.oauth2) { + parts.push(renderOAuth2AuthInterface(authRequirements.oauth2Schemes)); parts.push(''); } // Add the AuthConfig union type - parts.push(renderAuthConfigType(requirements)); + parts.push(renderAuthConfigType(authRequirements)); return parts.join('\n'); } @@ -570,7 +571,7 @@ export function renderHttpCommonTypes( securitySchemes?: ExtractedSecurityScheme[] ): string { const requirements = analyzeSecuritySchemes(securitySchemes); - const securityTypes = renderSecurityTypes(securitySchemes); + const securityTypes = renderSecurityTypes(securitySchemes, requirements); return `// ============================================================================ // Common Types - Shared across all HTTP client functions @@ -663,6 +664,15 @@ const AUTH_FEATURES = { oauth2: ${requirements.oauth2} } as const; +/** + * Default values for API key authentication derived from the spec. + * These match the defaults documented in the ApiKeyAuth interface. + */ +const API_KEY_DEFAULTS = { + name: '${requirements.apiKeySchemes.length === 1 ? requirements.apiKeySchemes[0].apiKeyName || 'X-API-Key' : 'X-API-Key'}', + in: '${requirements.apiKeySchemes.length === 1 ? requirements.apiKeySchemes[0].apiKeyIn || 'header' : 'header'}' as 'header' | 'query' | 'cookie' +} as const; + // ============================================================================ // Pagination Types // ============================================================================ @@ -855,14 +865,16 @@ function applyAuth( } case 'apiKey': { - const keyName = auth.name ?? 'X-API-Key'; - const keyIn = auth.in ?? 'header'; + const keyName = auth.name ?? API_KEY_DEFAULTS.name; + const keyIn = auth.in ?? API_KEY_DEFAULTS.in; if (keyIn === 'header') { headers[keyName] = auth.key; - } else { + } else if (keyIn === 'query') { const separator = url.includes('?') ? '&' : '?'; url = \`\${url}\${separator}\${keyName}=\${encodeURIComponent(auth.key)}\`; + } else if (keyIn === 'cookie') { + headers['Cookie'] = \`\${keyName}=\${auth.key}\`; } break; } From 6ef33d1e6f070dbe0509f7d83bbf5ce557456eff Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Mon, 9 Mar 2026 09:50:03 +0100 Subject: [PATCH 6/9] fix: escape OpenAPI spec values in generated TypeScript code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add escapeStringForCodeGen helper to escape special characters (backslashes, single quotes, backticks, dollar signs) in OpenAPI spec values before interpolating them into generated TypeScript. Prevents syntax errors when spec values contain characters like quotes in apiKeyName, tokenUrl, etc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../channels/protocols/http/fetch.ts | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts index a5a27892..f9c886d6 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts @@ -9,6 +9,19 @@ import { // Re-export for use by other modules export {ExtractedSecurityScheme}; +/** + * Escapes special characters in strings that will be interpolated into generated code. + * Prevents syntax errors when OpenAPI spec values contain quotes, backticks, or template expressions. + */ +function escapeStringForCodeGen(value: string | undefined): string { + if (!value) return ''; + return value + .replace(/\\/g, '\\\\') // Escape backslashes first + .replace(/'/g, "\\'") // Escape single quotes + .replace(/`/g, '\\`') // Escape backticks + .replace(/\$/g, '\\$'); // Escape dollar signs (prevents ${} template evaluation) +} + /** * Determines which auth types are needed based on security schemes. */ @@ -119,14 +132,18 @@ function renderApiKeyAuthInterface( ? "'header' | 'query' | 'cookie'" : "'header' | 'query'"; + // Escape spec values for safe interpolation into generated code + const escapedDefaultName = escapeStringForCodeGen(defaultName); + const escapedDefaultIn = escapeStringForCodeGen(defaultIn); + return `/** * API key authentication configuration */ export interface ApiKeyAuth { type: 'apiKey'; key: string; - name?: string; // Name of the API key parameter (default: '${defaultName}') - in?: ${inType}; // Where to place the API key (default: '${defaultIn}') + name?: string; // Name of the API key parameter (default: '${escapedDefaultName}') + in?: ${inType}; // Where to place the API key (default: '${escapedDefaultIn}') }`; } @@ -206,7 +223,7 @@ function extractSchemeComments( if (scheme.openIdConnectUrl) { return { ...existing, - tokenUrlComment: `OpenID Connect URL: '${scheme.openIdConnectUrl}'` + tokenUrlComment: `OpenID Connect URL: '${escapeStringForCodeGen(scheme.openIdConnectUrl)}'` }; } @@ -220,10 +237,10 @@ function extractSchemeComments( return { tokenUrlComment: tokenUrl - ? `default: '${tokenUrl}'` + ? `default: '${escapeStringForCodeGen(tokenUrl)}'` : existing.tokenUrlComment, authorizationUrlComment: authUrl - ? ` Authorization URL: '${authUrl}'` + ? ` Authorization URL: '${escapeStringForCodeGen(authUrl)}'` : existing.authorizationUrlComment, scopesComment: formatScopesComment(allScopes) || existing.scopesComment }; @@ -669,8 +686,8 @@ const AUTH_FEATURES = { * These match the defaults documented in the ApiKeyAuth interface. */ const API_KEY_DEFAULTS = { - name: '${requirements.apiKeySchemes.length === 1 ? requirements.apiKeySchemes[0].apiKeyName || 'X-API-Key' : 'X-API-Key'}', - in: '${requirements.apiKeySchemes.length === 1 ? requirements.apiKeySchemes[0].apiKeyIn || 'header' : 'header'}' as 'header' | 'query' | 'cookie' + name: '${escapeStringForCodeGen(requirements.apiKeySchemes.length === 1 ? requirements.apiKeySchemes[0].apiKeyName || 'X-API-Key' : 'X-API-Key')}', + in: '${escapeStringForCodeGen(requirements.apiKeySchemes.length === 1 ? requirements.apiKeySchemes[0].apiKeyIn || 'header' : 'header')}' as 'header' | 'query' | 'cookie' } as const; // ============================================================================ From 8663c47f738a1cef807de6d92706d194809fb7c8 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Mon, 9 Mar 2026 10:19:15 +0100 Subject: [PATCH 7/9] refactor: extract shared API key defaults logic into helper function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR review comment about duplicated logic between renderApiKeyAuthInterface and API_KEY_DEFAULTS template interpolation. Both now call getApiKeyDefaults() to ensure consistency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../channels/protocols/http/fetch.ts | 37 +++++++++++++------ .../src/openapi/channels/http_client.ts | 17 +++++++-- .../src/request-reply/channels/http_client.ts | 17 +++++++-- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts index f9c886d6..708d61c2 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts @@ -112,20 +112,33 @@ export interface BasicAuth { }`; } +/** + * Extracts API key defaults from schemes. + * If there's exactly one apiKey scheme, use its values; otherwise use standard defaults. + */ +function getApiKeyDefaults(apiKeySchemes: ExtractedSecurityScheme[]): { + name: string; + in: string; +} { + if (apiKeySchemes.length === 1) { + return { + name: apiKeySchemes[0].apiKeyName || 'X-API-Key', + in: apiKeySchemes[0].apiKeyIn || 'header' + }; + } + return { + name: 'X-API-Key', + in: 'header' + }; +} + /** * Generates the ApiKeyAuth interface with optional pre-populated defaults from spec. */ function renderApiKeyAuthInterface( apiKeySchemes: ExtractedSecurityScheme[] ): string { - // If there's exactly one apiKey scheme, we can provide defaults - let defaultName = 'X-API-Key'; - let defaultIn: string = 'header'; - - if (apiKeySchemes.length === 1) { - defaultName = apiKeySchemes[0].apiKeyName || defaultName; - defaultIn = apiKeySchemes[0].apiKeyIn || defaultIn; - } + const defaults = getApiKeyDefaults(apiKeySchemes); // For cookie support const inType = apiKeySchemes.some((s) => s.apiKeyIn === 'cookie') @@ -133,8 +146,8 @@ function renderApiKeyAuthInterface( : "'header' | 'query'"; // Escape spec values for safe interpolation into generated code - const escapedDefaultName = escapeStringForCodeGen(defaultName); - const escapedDefaultIn = escapeStringForCodeGen(defaultIn); + const escapedDefaultName = escapeStringForCodeGen(defaults.name); + const escapedDefaultIn = escapeStringForCodeGen(defaults.in); return `/** * API key authentication configuration @@ -686,8 +699,8 @@ const AUTH_FEATURES = { * These match the defaults documented in the ApiKeyAuth interface. */ const API_KEY_DEFAULTS = { - name: '${escapeStringForCodeGen(requirements.apiKeySchemes.length === 1 ? requirements.apiKeySchemes[0].apiKeyName || 'X-API-Key' : 'X-API-Key')}', - in: '${escapeStringForCodeGen(requirements.apiKeySchemes.length === 1 ? requirements.apiKeySchemes[0].apiKeyIn || 'header' : 'header')}' as 'header' | 'query' | 'cookie' + name: '${escapeStringForCodeGen(getApiKeyDefaults(requirements.apiKeySchemes).name)}', + in: '${escapeStringForCodeGen(getApiKeyDefaults(requirements.apiKeySchemes).in)}' as 'header' | 'query' | 'cookie' } as const; // ============================================================================ diff --git a/test/runtime/typescript/src/openapi/channels/http_client.ts b/test/runtime/typescript/src/openapi/channels/http_client.ts index fa3d0ed3..06f1f8a9 100644 --- a/test/runtime/typescript/src/openapi/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi/channels/http_client.ts @@ -156,6 +156,15 @@ const AUTH_FEATURES = { oauth2: true } as const; +/** + * Default values for API key authentication derived from the spec. + * These match the defaults documented in the ApiKeyAuth interface. + */ +const API_KEY_DEFAULTS = { + name: 'api_key', + in: 'header' as 'header' | 'query' | 'cookie' +} as const; + // ============================================================================ // Pagination Types // ============================================================================ @@ -348,14 +357,16 @@ function applyAuth( } case 'apiKey': { - const keyName = auth.name ?? 'X-API-Key'; - const keyIn = auth.in ?? 'header'; + const keyName = auth.name ?? API_KEY_DEFAULTS.name; + const keyIn = auth.in ?? API_KEY_DEFAULTS.in; if (keyIn === 'header') { headers[keyName] = auth.key; - } else { + } else if (keyIn === 'query') { const separator = url.includes('?') ? '&' : '?'; url = `${url}${separator}${keyName}=${encodeURIComponent(auth.key)}`; + } else if (keyIn === 'cookie') { + headers['Cookie'] = `${keyName}=${auth.key}`; } break; } diff --git a/test/runtime/typescript/src/request-reply/channels/http_client.ts b/test/runtime/typescript/src/request-reply/channels/http_client.ts index 8297360d..f4d913a8 100644 --- a/test/runtime/typescript/src/request-reply/channels/http_client.ts +++ b/test/runtime/typescript/src/request-reply/channels/http_client.ts @@ -173,6 +173,15 @@ const AUTH_FEATURES = { oauth2: true } as const; +/** + * Default values for API key authentication derived from the spec. + * These match the defaults documented in the ApiKeyAuth interface. + */ +const API_KEY_DEFAULTS = { + name: 'X-API-Key', + in: 'header' as 'header' | 'query' | 'cookie' +} as const; + // ============================================================================ // Pagination Types // ============================================================================ @@ -365,14 +374,16 @@ function applyAuth( } case 'apiKey': { - const keyName = auth.name ?? 'X-API-Key'; - const keyIn = auth.in ?? 'header'; + const keyName = auth.name ?? API_KEY_DEFAULTS.name; + const keyIn = auth.in ?? API_KEY_DEFAULTS.in; if (keyIn === 'header') { headers[keyName] = auth.key; - } else { + } else if (keyIn === 'query') { const separator = url.includes('?') ? '&' : '?'; url = `${url}${separator}${keyName}=${encodeURIComponent(auth.key)}`; + } else if (keyIn === 'cookie') { + headers['Cookie'] = `${keyName}=${auth.key}`; } break; } From 0118d2115f5759cd7990413b0c09213daf403f29 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Mon, 9 Mar 2026 10:50:13 +0100 Subject: [PATCH 8/9] fix: update tests for API key defaults from spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated code now uses 'api_key' as the default header name (extracted from the OpenAPI spec) instead of the generic 'X-API-Key'. Updated the runtime test to expect this correct header name and updated snapshots to reflect the new generated output. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../__snapshots__/channels.spec.ts.snap | 742 +++++++++--------- .../http_client/security_schemes.spec.ts | 6 +- 2 files changed, 392 insertions(+), 356 deletions(-) diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index ada7fa1d..5eab2593 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -150,6 +150,23 @@ export interface OAuth2Auth { */ export type AuthConfig = ApiKeyAuth | OAuth2Auth; +/** + * Feature flags indicating which auth types are available. + * Used internally to conditionally call auth-specific helpers. + */ +const AUTH_FEATURES = { + oauth2: true +} as const; + +/** + * Default values for API key authentication derived from the spec. + * These match the defaults documented in the ApiKeyAuth interface. + */ +const API_KEY_DEFAULTS = { + name: 'api_key', + in: 'header' as 'header' | 'query' | 'cookie' +} as const; + // ============================================================================ // Pagination Types // ============================================================================ @@ -342,14 +359,16 @@ function applyAuth( } case 'apiKey': { - const keyName = auth.name ?? 'X-API-Key'; - const keyIn = auth.in ?? 'header'; + const keyName = auth.name ?? API_KEY_DEFAULTS.name; + const keyIn = auth.in ?? API_KEY_DEFAULTS.in; if (keyIn === 'header') { headers[keyName] = auth.key; - } else { + } else if (keyIn === 'query') { const separator = url.includes('?') ? '&' : '?'; url = \`\${url}\${separator}\${keyName}=\${encodeURIComponent(auth.key)}\`; + } else if (keyIn === 'cookie') { + headers['Cookie'] = \`\${keyName}=\${auth.key}\`; } break; } @@ -367,34 +386,6 @@ function applyAuth( return { headers, url }; } -/** - * Validate OAuth2 configuration based on flow type - */ -function validateOAuth2Config(auth: OAuth2Auth): void { - // If using a flow, validate required fields - switch (auth.flow) { - case 'client_credentials': - if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); - break; - - case 'password': - if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); - if (!auth.username) throw new Error('OAuth2 Password flow requires username'); - if (!auth.password) throw new Error('OAuth2 Password flow requires password'); - break; - - default: - // No flow specified - must have accessToken for OAuth2 to work - if (!auth.accessToken && !auth.flow) { - // This is fine - token refresh can still work if refreshToken is provided - // Or the request will just be made without auth - } - break; - } -} - /** * Apply pagination parameters to URL and/or headers based on configuration */ @@ -584,126 +575,6 @@ async function executeWithRetry( throw lastError ?? new Error('Request failed after retries'); } -/** - * Handle OAuth2 token flows (client_credentials, password) - */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); - authHeaders['Authorization'] = \`Basic \${credentials}\`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -976,56 +847,203 @@ function applyTypedHeaders( return headers; } -// ============================================================================ -// Generated HTTP Client Functions -// ============================================================================ - -export interface PostAddPetContext extends HttpClientContext { - payload: Pet; - requestHeaders?: { marshal: () => string }; -} +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; -async function postAddPet(context: PostAddPetContext): Promise> { - // Apply defaults - const config = { - path: '/pet', - server: 'localhost:3000', - ...context, - }; + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; - // Validate OAuth2 config if present - if (config.auth?.type === 'oauth2') { - validateOAuth2Config(config.auth); + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; } +} - // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; - // Build URL - let url = \`\${config.server}\${config.path}\`; - url = applyQueryParams(config.queryParams, url); + const params = new URLSearchParams(); - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; + } - // Apply authentication - const authResult = applyAuth(config.auth, headers, url); - headers = authResult.headers; - url = authResult.url; + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } - // Prepare body - const body = context.payload?.marshal(); + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; - // Determine request function - const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); + authHeaders['Authorization'] = \`Basic \${credentials}\`; + params.delete('client_id'); + params.delete('client_secret'); + } - // Build request params - let requestParams: HttpRequestParams = { - url, + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} +// ============================================================================ +// Generated HTTP Client Functions +// ============================================================================ + +export interface PostAddPetContext extends HttpClientContext { + payload: Pet; + requestHeaders?: { marshal: () => string }; +} + +async function postAddPet(context: PostAddPetContext): Promise> { + // Apply defaults + const config = { + path: '/pet', + server: 'localhost:3000', + ...context, + }; + + // Validate OAuth2 config if present + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + validateOAuth2Config(config.auth); + } + + // Build headers + let headers = context.requestHeaders + ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) + : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + + // Build URL + let url = \`\${config.server}\${config.path}\`; + url = applyQueryParams(config.queryParams, url); + + // Apply pagination (can affect URL and/or headers) + const paginationResult = applyPagination(config.pagination, url, headers); + url = paginationResult.url; + headers = paginationResult.headers; + + // Apply authentication + const authResult = applyAuth(config.auth, headers, url); + headers = authResult.headers; + url = authResult.url; + + // Prepare body + const body = context.payload?.marshal(); + + // Determine request function + const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; + + // Build request params + let requestParams: HttpRequestParams = { + url, method: 'POST', headers, body @@ -1046,7 +1064,7 @@ async function postAddPet(context: PostAddPetContext): Promise Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); - authHeaders['Authorization'] = \`Basic \${credentials}\`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -2873,6 +2762,153 @@ function applyTypedHeaders( return headers; } +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; + + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; + + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; + } +} + +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; + + const params = new URLSearchParams(); + + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; + } + + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } + + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); + authHeaders['Authorization'] = \`Basic \${credentials}\`; + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} // ============================================================================ // Generated HTTP Client Functions // ============================================================================ @@ -2890,7 +2926,7 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise { let receivedApiKey: string | undefined; // The spec defines: name: "api_key", in: "header" - // The generated interface has this as the default in the comment + // The generated code uses 'api_key' as the default header name (from spec) router.post('/pet', (req, res) => { - receivedApiKey = req.headers['x-api-key'] as string; + receivedApiKey = req.headers['api_key'] as string; res.setHeader('Content-Type', 'application/json'); res.write(responsePet.marshal()); res.end(); @@ -88,7 +88,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { auth: { type: 'apiKey', key: 'my-secret-api-key' - // Uses default header name 'X-API-Key' when not specified + // Uses default header name 'api_key' from spec when not specified } }); From 20fceeeec95c68e6b339a44ebf30abf062bfd9c2 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Mon, 9 Mar 2026 10:55:53 +0100 Subject: [PATCH 9/9] fix: resolve ESLint errors in fetch.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add curly braces after if condition - Remove multiple spaces before inline comments 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../typescript/channels/protocols/http/fetch.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts index 708d61c2..24906792 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/fetch.ts @@ -14,12 +14,14 @@ export {ExtractedSecurityScheme}; * Prevents syntax errors when OpenAPI spec values contain quotes, backticks, or template expressions. */ function escapeStringForCodeGen(value: string | undefined): string { - if (!value) return ''; + if (!value) { + return ''; + } return value - .replace(/\\/g, '\\\\') // Escape backslashes first - .replace(/'/g, "\\'") // Escape single quotes - .replace(/`/g, '\\`') // Escape backticks - .replace(/\$/g, '\\$'); // Escape dollar signs (prevents ${} template evaluation) + .replace(/\\/g, '\\\\') // Escape backslashes first + .replace(/'/g, "\\'") // Escape single quotes + .replace(/`/g, '\\`') // Escape backticks + .replace(/\$/g, '\\$'); // Escape dollar signs (prevents ${} template evaluation) } /**