diff --git a/src/lib/resource-uri.ts b/src/lib/resource-uri.ts index a0c2403..bd6b3a0 100644 --- a/src/lib/resource-uri.ts +++ b/src/lib/resource-uri.ts @@ -130,6 +130,17 @@ export function parseArmUri( */ export function buildResourceLabel(descriptor: ResourceDescriptor): string { const metadata = RESOURCE_TYPE_METADATA[descriptor.type]; + + // Association types (e.g. GatewayApi) are sometimes discovered at the + // parent-association-file level — a descriptor carrying only the parent's + // name-parts (e.g. GatewayApi ['my-gateway'] representing + // gateways/my-gateway/apis.json before per-API entries are expanded). + // armPathSuffix has more placeholders than such a descriptor can fill, so + // fall back to artifactDirectory (which only needs the parent's parts). + if (descriptor.nameParts.length < countTemplatePlaceholders(metadata.armPathSuffix)) { + return formatTemplatePath(metadata.artifactDirectory, descriptor.nameParts); + } + // armPathSuffix has no leading slash, so the result is already relative return formatTemplatePath(metadata.armPathSuffix, descriptor.nameParts); } diff --git a/src/services/api-publisher.ts b/src/services/api-publisher.ts index 265ecb3..e2b42a6 100644 --- a/src/services/api-publisher.ts +++ b/src/services/api-publisher.ts @@ -276,6 +276,7 @@ export async function planApiPublication( ); let importSpecification = false; + let importSpecificationFormat: string | undefined; let importSpecSchemaComponents: Record | undefined; let operationDescriptionPuts: ResourceDescriptor[] = []; if (specificationAllowed) { @@ -286,6 +287,7 @@ export async function planApiPublication( const dialect = detectSpecDialect(specification.content, specification.format); importSpecification = getImportFormat(specification.format ?? 'yaml', apiType, dialect) !== undefined; + importSpecificationFormat = importSpecification ? specification.format : undefined; if (importSpecification) { importSpecSchemaComponents = getSpecComponentSchemas( specification.content, @@ -365,15 +367,31 @@ export async function planApiPublication( }) ) ).filter((descriptor): descriptor is ResourceDescriptor => descriptor !== undefined); + // WSDL-import-generated XSD schemas have GUID names (not 24-hex), so the + // isAutoGeneratedId filter misses them. APIM recreates them on WSDL import + // and rejects re-PUTs, so exclude schemas with the XSD content type — but + // only for WSDL imports; other importers do not recreate XSD schemas. + const nonImporterSchemas = importSpecificationFormat !== 'wsdl' + ? explicitSchemas + : ( + await Promise.all( + explicitSchemas.map(async (descriptor) => { + const schemaJson = await store.readResource(config.sourceDir, descriptor); + const contentType = (schemaJson?.properties as Record | undefined) + ?.contentType; + return contentType === 'application/vnd.ms-azure-apim.xsd+xml' ? undefined : descriptor; + }) + ) + ).filter((descriptor): descriptor is ResourceDescriptor => descriptor !== undefined); const filteredExplicitSchemas = config.filter - ? explicitSchemas.filter((descriptor) => + ? nonImporterSchemas.filter((descriptor) => shouldIncludeResource(descriptor, effectiveFilter) ) : allowed - ? explicitSchemas.filter((descriptor) => + ? nonImporterSchemas.filter((descriptor) => allowed.has(getResourceDescriptorKey(descriptor)) ) - : explicitSchemas; + : nonImporterSchemas; childPuts = [...childPuts, ...filteredExplicitSchemas]; } diff --git a/tests/unit/lib/resource-uri.test.ts b/tests/unit/lib/resource-uri.test.ts index 5df4dc2..60674f8 100644 --- a/tests/unit/lib/resource-uri.test.ts +++ b/tests/unit/lib/resource-uri.test.ts @@ -251,6 +251,26 @@ describe('buildResourceLabel', () => { expect(label).toBe('apis/petstore/operations/get-user'); }); + it('should fall back to artifact directory for parent-level association descriptor (GatewayApi)', () => { + // GatewayApi discovered from gateways/{gateway}/apis.json carries only the + // gateway name; armPathSuffix needs two parts and used to throw fatally. + const descriptor: ResourceDescriptor = { + type: ResourceType.GatewayApi, + nameParts: ['my-gateway'], + }; + const label = buildResourceLabel(descriptor); + expect(label).toBe('gateways/my-gateway'); + }); + + it('should format fully-expanded association descriptor (GatewayApi)', () => { + const descriptor: ResourceDescriptor = { + type: ResourceType.GatewayApi, + nameParts: ['my-gateway', 'my-api'], + }; + const label = buildResourceLabel(descriptor); + expect(label).toBe('gateways/my-gateway/apis/my-api'); + }); + it('should format grandchild policy resource (ApiOperationPolicy)', () => { const descriptor: ResourceDescriptor = { type: ResourceType.ApiOperationPolicy, diff --git a/tests/unit/services/api-publisher.test.ts b/tests/unit/services/api-publisher.test.ts index ced8362..3446074 100644 --- a/tests/unit/services/api-publisher.test.ts +++ b/tests/unit/services/api-publisher.test.ts @@ -1894,6 +1894,75 @@ describe('api-publisher', () => { expect(totalTasks).toBe(1); }); + it('should skip WSDL-importer-generated XSD schemas (GUID names) on WSDL import', async () => { + const client = createMockClient(); + // GUID name is not caught by isAutoGeneratedId (24-hex only), but the + // WSDL importer recreates XSD schemas, so re-PUTs must be excluded. + const xsdSchema = { + type: ResourceType.ApiSchema, + nameParts: ['soap-api', '4b6fe4d6-dbad-4db0-a6d6-58958e74850d'], + }; + const store = createMockStore([xsdSchema]); + store.readResource.mockImplementation(async (_dir: string, descriptor: ResourceDescriptor) => { + if (descriptor.type === ResourceType.Api) { + return { name: 'soap-api', properties: { path: 'soap', type: 'soap' } }; + } + if (descriptor.type === ResourceType.ApiSchema) { + return { + name: descriptor.nameParts[1], + properties: { contentType: 'application/vnd.ms-azure-apim.xsd+xml', document: { value: '' } }, + }; + } + return null; + }); + store.readContent.mockResolvedValue({ + content: '...', + format: 'wsdl', + }); + + const apiDescriptor: ResourceDescriptor = { type: ResourceType.Api, nameParts: ['soap-api'] }; + await publishApi(client, store, testContext, apiDescriptor, testConfig); + + const totalTasks = mockRunParallel.mock.calls.reduce((sum, call) => { + const tasks = call[0] as unknown[]; + return sum + tasks.length; + }, 0); + expect(totalTasks).toBe(0); + }); + + it('should retain GUID-named XSD schemas for non-WSDL spec imports', async () => { + const client = createMockClient(); + // OpenAPI import does not recreate XSD schemas, so the exclusion must + // apply only when the imported spec format is wsdl. + const xsdSchema = { + type: ResourceType.ApiSchema, + nameParts: ['rest-api', '4b6fe4d6-dbad-4db0-a6d6-58958e74850d'], + }; + const store = createMockStore([xsdSchema]); + store.readResource.mockImplementation(async (_dir: string, descriptor: ResourceDescriptor) => { + if (descriptor.type === ResourceType.Api) { + return { name: 'rest-api', properties: { path: 'rest' } }; + } + if (descriptor.type === ResourceType.ApiSchema) { + return { + name: descriptor.nameParts[1], + properties: { contentType: 'application/vnd.ms-azure-apim.xsd+xml', document: { value: '' } }, + }; + } + return null; + }); + store.readContent.mockResolvedValue({ content: 'openapi: "3.0.0"', format: 'yaml' }); + + const apiDescriptor: ResourceDescriptor = { type: ResourceType.Api, nameParts: ['rest-api'] }; + await publishApi(client, store, testContext, apiDescriptor, testConfig); + + const totalTasks = mockRunParallel.mock.calls.reduce((sum, call) => { + const tasks = call[0] as unknown[]; + return sum + tasks.length; + }, 0); + expect(totalTasks).toBe(1); + }); + it('should skip 13-digit portal schema when the imported spec recreates its components', async () => { const client = createMockClient(); // The portal assigns Date.now()-style schema IDs; when the imported spec