From a77dc24ef8c65a1046c663aa4dc96c26dbb61003 Mon Sep 17 00:00:00 2001 From: Deepa Maria Polson Date: Sun, 1 Feb 2026 09:54:39 +0530 Subject: [PATCH] feat: spec change --- .../parser/src/models/v3/operation-trait.ts | 32 +++- packages/parser/src/models/v3/server.ts | 32 +++- packages/parser/src/spec-types/v3.ts | 8 +- .../test/models/v3/operation-trait.spec.ts | 138 +++++++++++++++++- packages/parser/test/models/v3/server.spec.ts | 136 ++++++++++++++++- 5 files changed, 333 insertions(+), 13 deletions(-) diff --git a/packages/parser/src/models/v3/operation-trait.ts b/packages/parser/src/models/v3/operation-trait.ts index dcc7dff0b..2527812ff 100644 --- a/packages/parser/src/models/v3/operation-trait.ts +++ b/packages/parser/src/models/v3/operation-trait.ts @@ -22,10 +22,34 @@ export class OperationTrait { - const scheme = this.createModel(SecurityScheme, security as v3.SecuritySchemeObject, { id: '', pointer: this.jsonPath(`security/${index}`) }); - const requirement = this.createModel(SecurityRequirement, { scheme, scopes: (security as v3.SecuritySchemeObject).scopes }, { id: '', pointer: this.jsonPath(`security/${index}`) }); - return new SecurityRequirements([requirement]); + const securitySchemes = (this._meta?.asyncapi?.parsed?.components?.securitySchemes || {}) as Record; + return (this._json.security || []).map((requirement, index) => { + const requirements: SecurityRequirement[] = []; + + // Check if this is a reference + const isReference = requirement && typeof requirement === 'object' && '$ref' in requirement; + // Check if this is a SecuritySchemeObject (has 'type' property) + const isSecurityScheme = requirement && typeof requirement === 'object' && 'type' in requirement; + + if (!isReference && !isSecurityScheme && requirement && typeof requirement === 'object') { + // New format: SecurityRequirementObject (Record>) + Object.entries(requirement as v3.SecurityRequirementObject).forEach(([security, schemes]) => { + schemes.forEach((schemeData, schemeIndex) => { + const scheme = this.createModel(SecurityScheme, securitySchemes[security] || schemeData, { id: security, pointer: `/components/securitySchemes/${security}` }); + requirements.push( + this.createModel(SecurityRequirement, { scheme, scopes: (schemeData as v3.SecuritySchemeObject).scopes || [] }, { id: security, pointer: `${this.meta().pointer}/security/${index}/${security}/${schemeIndex}` }) + ); + }); + }); + } else { + // Old format: direct SecuritySchemeObject or ReferenceObject (for backward compatibility) + const scheme = this.createModel(SecurityScheme, requirement as v3.SecuritySchemeObject, { id: '', pointer: this.jsonPath(`security/${index}`) }); + requirements.push( + this.createModel(SecurityRequirement, { scheme, scopes: (requirement as v3.SecuritySchemeObject).scopes || [] }, { id: '', pointer: this.jsonPath(`security/${index}`) }) + ); + } + + return new SecurityRequirements(requirements); }); } } diff --git a/packages/parser/src/models/v3/server.ts b/packages/parser/src/models/v3/server.ts index 9308882dc..94d1d888e 100644 --- a/packages/parser/src/models/v3/server.ts +++ b/packages/parser/src/models/v3/server.ts @@ -116,10 +116,34 @@ export class Server extends CoreModel implement } security(): SecurityRequirements[] { - return (this._json.security || []).map((security, index) => { - const scheme = this.createModel(SecurityScheme, security as v3.SecuritySchemeObject, { id: '', pointer: this.jsonPath(`security/${index}`) }); - const requirement = this.createModel(SecurityRequirement, { scheme, scopes: (security as v3.SecuritySchemeObject).scopes }, { id: '', pointer: this.jsonPath(`security/${index}`) }); - return new SecurityRequirements([requirement]); + const securitySchemes = (this._meta?.asyncapi?.parsed?.components?.securitySchemes || {}) as Record; + return (this._json.security || []).map((requirement, index) => { + const requirements: SecurityRequirement[] = []; + + // Check if this is a reference + const isReference = requirement && typeof requirement === 'object' && '$ref' in requirement; + // Check if this is a SecuritySchemeObject (has 'type' property) + const isSecurityScheme = requirement && typeof requirement === 'object' && 'type' in requirement; + + if (!isReference && !isSecurityScheme && requirement && typeof requirement === 'object') { + // New format: SecurityRequirementObject (Record>) + Object.entries(requirement as v3.SecurityRequirementObject).forEach(([security, schemes]) => { + schemes.forEach((schemeData, schemeIndex) => { + const scheme = this.createModel(SecurityScheme, securitySchemes[security] || schemeData, { id: security, pointer: `/components/securitySchemes/${security}` }); + requirements.push( + this.createModel(SecurityRequirement, { scheme, scopes: (schemeData as v3.SecuritySchemeObject).scopes || [] }, { id: security, pointer: `${this.meta().pointer}/security/${index}/${security}/${schemeIndex}` }) + ); + }); + }); + } else { + // Old format: direct SecuritySchemeObject or ReferenceObject (for backward compatibility) + const scheme = this.createModel(SecurityScheme, requirement as v3.SecuritySchemeObject, { id: '', pointer: this.jsonPath(`security/${index}`) }); + requirements.push( + this.createModel(SecurityRequirement, { scheme, scopes: (requirement as v3.SecuritySchemeObject).scopes || [] }, { id: '', pointer: this.jsonPath(`security/${index}`) }) + ); + } + + return new SecurityRequirements(requirements); }); } } diff --git a/packages/parser/src/spec-types/v3.ts b/packages/parser/src/spec-types/v3.ts index 766df3bf9..59573b038 100644 --- a/packages/parser/src/spec-types/v3.ts +++ b/packages/parser/src/spec-types/v3.ts @@ -46,7 +46,7 @@ export interface ServerObject extends SpecificationExtensions { protocolVersion?: string; description?: string; variables?: Record; - security?: Array; + security?: Array; tags?: TagsObject; externalDocs?: ExternalDocumentationObject | ReferenceObject; bindings?: ServerBindingsObject | ReferenceObject; @@ -124,7 +124,7 @@ export interface OperationObject extends SpecificationExtensions { title?: string; summary?: string; description?: string; - security?: Array; + security?: Array; tags?: TagsObject; externalDocs?: ExternalDocumentationObject | ReferenceObject; bindings?: OperationBindingsObject | ReferenceObject; @@ -135,7 +135,7 @@ export interface OperationTraitObject extends SpecificationExtensions { title?: string; summary?: string; description?: string; - security?: Array; + security?: Array; tags?: TagsObject; externalDocs?: ExternalDocumentationObject | ReferenceObject; bindings?: OperationBindingsObject | ReferenceObject; @@ -277,6 +277,8 @@ export interface SecuritySchemeObject extends SpecificationExtensions { scopes?: string[]; } +export type SecurityRequirementObject = Record>; + export type SecuritySchemeType = | 'userPassword' | 'apiKey' diff --git a/packages/parser/test/models/v3/operation-trait.spec.ts b/packages/parser/test/models/v3/operation-trait.spec.ts index e211e8fce..d71656b31 100644 --- a/packages/parser/test/models/v3/operation-trait.spec.ts +++ b/packages/parser/test/models/v3/operation-trait.spec.ts @@ -33,7 +33,7 @@ describe('OperationTrait model', function() { }); describe('.security()', function() { - it('should return collection of security requirements', function() { + it('should return collection of security requirements - old format (direct SecuritySchemeObject)', function() { const d = new OperationTrait({ security: [{ type: 'apiKey' }] }); const security = d.security(); @@ -46,6 +46,105 @@ describe('OperationTrait model', function() { expect(requirement.scheme()).toBeInstanceOf(SecurityScheme); expect(requirement.scopes()).toEqual([]); }); + + it('should return collection of security requirements - new format (SecurityRequirementObject)', function() { + const securitySchemes = { + apiKey: { type: 'apiKey', in: 'user' }, + oauth2: { type: 'oauth2', flows: {}, scopes: ['read', 'write'] } + }; + const d = new OperationTrait( + { + security: [ + { + apiKey: [{ type: 'apiKey', in: 'user' }], + oauth2: [{ type: 'oauth2', flows: {}, scopes: ['read', 'write'] }] + } + ] + }, + { + asyncapi: { + parsed: { + components: { + securitySchemes + } + } + } + } as any + ); + + const security = d.security(); + expect(Array.isArray(security)).toEqual(true); + expect(security).toHaveLength(1); + expect(security[0]).toBeInstanceOf(SecurityRequirements); + + const requirements = security[0].all(); + expect(requirements).toHaveLength(2); + + // First requirement (apiKey) + const requirement1 = requirements[0] as SecurityRequirement; + expect(requirement1).toBeInstanceOf(SecurityRequirement); + expect(requirement1.scheme()).toBeInstanceOf(SecurityScheme); + expect(requirement1.scheme().type()).toEqual('apiKey'); + expect(requirement1.scopes()).toEqual([]); + + // Second requirement (oauth2) + const requirement2 = requirements[1] as SecurityRequirement; + expect(requirement2).toBeInstanceOf(SecurityRequirement); + expect(requirement2.scheme()).toBeInstanceOf(SecurityScheme); + expect(requirement2.scheme().type()).toEqual('oauth2'); + expect(requirement2.scopes()).toEqual(['read', 'write']); + }); + + it('should return collection of security requirements - new format with multiple schemes in array', function() { + const securitySchemes = { + apiKey: { type: 'apiKey', in: 'user' } + }; + const d = new OperationTrait( + { + security: [ + { + apiKey: [ + { type: 'apiKey', in: 'user' }, + { type: 'apiKey', in: 'password' } + ] + } + ] + }, + { + asyncapi: { + parsed: { + components: { + securitySchemes + } + } + } + } as any + ); + + const security = d.security(); + expect(Array.isArray(security)).toEqual(true); + expect(security).toHaveLength(1); + + const requirements = security[0].all(); + expect(requirements).toHaveLength(2); + + requirements.forEach((req) => { + expect(req).toBeInstanceOf(SecurityRequirement); + expect(req.scheme()).toBeInstanceOf(SecurityScheme); + expect(req.scheme().type()).toEqual('apiKey'); + }); + }); + + it('should return collection of security requirements - old format with reference', function() { + const d = new OperationTrait({ + security: [{ $ref: '#/components/securitySchemes/apiKey' }] + }); + + const security = d.security(); + expect(Array.isArray(security)).toEqual(true); + expect(security).toHaveLength(1); + expect(security[0]).toBeInstanceOf(SecurityRequirements); + }); it('should return collection of security requirements when value is undefined', function() { const doc = {}; @@ -53,6 +152,43 @@ describe('OperationTrait model', function() { expect(Array.isArray(d.security())).toEqual(true); expect(d.security()).toHaveLength(0); }); + + it('should handle mixed security requirements', function() { + const securitySchemes = { + apiKey: { type: 'apiKey', in: 'user' } + }; + const d = new OperationTrait( + { + security: [ + { type: 'http', scheme: 'bearer' }, // Old format + { apiKey: [{ type: 'apiKey', in: 'user' }] } // New format + ] + }, + { + asyncapi: { + parsed: { + components: { + securitySchemes + } + } + } + } as any + ); + + const security = d.security(); + expect(Array.isArray(security)).toEqual(true); + expect(security).toHaveLength(2); + + // First requirement (old format) + expect(security[0]).toBeInstanceOf(SecurityRequirements); + const req1 = security[0].all()[0] as SecurityRequirement; + expect(req1.scheme().type()).toEqual('http'); + + // Second requirement (new format) + expect(security[1]).toBeInstanceOf(SecurityRequirements); + const req2 = security[1].all()[0] as SecurityRequirement; + expect(req2.scheme().type()).toEqual('apiKey'); + }); }); describe('mixins', function() { diff --git a/packages/parser/test/models/v3/server.spec.ts b/packages/parser/test/models/v3/server.spec.ts index 2669bc8dd..c91c7183d 100644 --- a/packages/parser/test/models/v3/server.spec.ts +++ b/packages/parser/test/models/v3/server.spec.ts @@ -207,7 +207,7 @@ describe('Server Model', function () { }); describe('.security()', function() { - it('should return SecurityRequirements', function() { + it('should return SecurityRequirements - old format (direct SecuritySchemeObject)', function() { const doc = serializeInput({ security: [{ type: 'apiKey' }] }); const d = new Server(doc, {pointer: '/servers/test'} as any); @@ -222,6 +222,104 @@ describe('Server Model', function () { expect(requirement.scopes()).toEqual([]); expect(requirement.meta().pointer).toEqual('/servers/test/security/0'); }); + + it('should return SecurityRequirements - new format (SecurityRequirementObject)', function() { + const securitySchemes = { + apiKey: { type: 'apiKey', in: 'user' }, + oauth2: { type: 'oauth2', flows: {}, scopes: ['read', 'write'] } + }; + const doc = serializeInput({ + security: [ + { + apiKey: [{ type: 'apiKey', in: 'user' }], + oauth2: [{ type: 'oauth2', flows: {}, scopes: ['read', 'write'] }] + } + ] + }); + const d = new Server(doc, { + pointer: '/servers/test', + asyncapi: { + parsed: { + components: { + securitySchemes + } + } + } + } as any); + + const security = d.security(); + expect(Array.isArray(security)).toEqual(true); + expect(security).toHaveLength(1); + expect(security[0]).toBeInstanceOf(SecurityRequirements); + + const requirements = security[0].all(); + expect(requirements).toHaveLength(2); + + // First requirement (apiKey) + const requirement1 = requirements[0] as SecurityRequirement; + expect(requirement1).toBeInstanceOf(SecurityRequirement); + expect(requirement1.scheme()).toBeInstanceOf(SecurityScheme); + expect(requirement1.scheme().type()).toEqual('apiKey'); + expect(requirement1.scopes()).toEqual([]); + + // Second requirement (oauth2) + const requirement2 = requirements[1] as SecurityRequirement; + expect(requirement2).toBeInstanceOf(SecurityRequirement); + expect(requirement2.scheme()).toBeInstanceOf(SecurityScheme); + expect(requirement2.scheme().type()).toEqual('oauth2'); + expect(requirement2.scopes()).toEqual(['read', 'write']); + }); + + it('should return SecurityRequirements - new format with multiple schemes in array', function() { + const securitySchemes = { + apiKey: { type: 'apiKey', in: 'user' } + }; + const doc = serializeInput({ + security: [ + { + apiKey: [ + { type: 'apiKey', in: 'user' }, + { type: 'apiKey', in: 'password' } + ] + } + ] + }); + const d = new Server(doc, { + pointer: '/servers/test', + asyncapi: { + parsed: { + components: { + securitySchemes + } + } + } + } as any); + + const security = d.security(); + expect(Array.isArray(security)).toEqual(true); + expect(security).toHaveLength(1); + + const requirements = security[0].all(); + expect(requirements).toHaveLength(2); + + requirements.forEach((req) => { + expect(req).toBeInstanceOf(SecurityRequirement); + expect(req.scheme()).toBeInstanceOf(SecurityScheme); + expect(req.scheme().type()).toEqual('apiKey'); + }); + }); + + it('should return SecurityRequirements - old format with reference', function() { + const doc = serializeInput({ + security: [{ $ref: '#/components/securitySchemes/apiKey' }] + }); + const d = new Server(doc, {pointer: '/servers/test'} as any); + + const security = d.security(); + expect(Array.isArray(security)).toEqual(true); + expect(security).toHaveLength(1); + expect(security[0]).toBeInstanceOf(SecurityRequirements); + }); it('should return SecurityRequirements when value is undefined', function() { const doc = serializeInput({}); @@ -229,6 +327,42 @@ describe('Server Model', function () { expect(Array.isArray(d.security())).toEqual(true); expect(d.security()).toHaveLength(0); }); + + it('should handle mixed security requirements', function() { + const securitySchemes = { + apiKey: { type: 'apiKey', in: 'user' } + }; + const doc = serializeInput({ + security: [ + { type: 'http', scheme: 'bearer' }, // Old format + { apiKey: [{ type: 'apiKey', in: 'user' }] } // New format + ] + }); + const d = new Server(doc, { + pointer: '/servers/test', + asyncapi: { + parsed: { + components: { + securitySchemes + } + } + } + } as any); + + const security = d.security(); + expect(Array.isArray(security)).toEqual(true); + expect(security).toHaveLength(2); + + // First requirement (old format) + expect(security[0]).toBeInstanceOf(SecurityRequirements); + const req1 = security[0].all()[0] as SecurityRequirement; + expect(req1.scheme().type()).toEqual('http'); + + // Second requirement (new format) + expect(security[1]).toBeInstanceOf(SecurityRequirements); + const req2 = security[1].all()[0] as SecurityRequirement; + expect(req2.scheme().type()).toEqual('apiKey'); + }); }); describe('mixins', function () {