diff --git a/docs/guides/environment-overrides.md b/docs/guides/environment-overrides.md index 30ebcba7..2dcfcc04 100644 --- a/docs/guides/environment-overrides.md +++ b/docs/guides/environment-overrides.md @@ -416,8 +416,6 @@ workspaces: The supported workspace child sections are: `apis`, `backends`, `diagnostics`, `groups`, `loggers`, `namedValues`, `policyFragments`, `products`, `subscriptions`, `tags`, and `versionSets`. -> ⚠️ **Known limitation — tracked in [#118](https://github.com/Azure/apiops-cli/issues/118):** workspace child overrides are *parsed* (the YAML above is accepted with no errors) but are **not yet applied at publish time**. Until #118 is fixed, only the workspace container's own `properties` are honored for workspace-scoped resources. Authoring overrides in this nested shape today is safe and forward-compatible — they will start taking effect automatically once the merger is updated. - ## Override rules ### Names must be consistent diff --git a/src/services/api-publisher.ts b/src/services/api-publisher.ts index f881de7c..4fbc7fa2 100644 --- a/src/services/api-publisher.ts +++ b/src/services/api-publisher.ts @@ -24,7 +24,7 @@ import { } from './resource-publisher.js'; import { mapDescriptor } from './env-mapper.js'; import { runParallel } from '../lib/parallel-runner.js'; -import { applyOverrides } from './override-merger.js'; +import { applyOverrides, resolveOverrideSection } from './override-merger.js'; import { logger } from '../lib/logger.js'; import { getNamePart, @@ -555,7 +555,7 @@ async function publishRootApi( json = normalizeApiAuthenticationSettings(json, { preferLegacyFields: prefersLegacyAuthOverride( getNamePart(descriptor.nameParts, 0), - config.overrides?.apis + resolveOverrideSection(config.overrides, 'apis', descriptor.workspace) ), }); const isCurrent = getApiIsCurrent(json); diff --git a/src/services/override-merger.ts b/src/services/override-merger.ts index e6d3a012..216a27e7 100644 --- a/src/services/override-merger.ts +++ b/src/services/override-merger.ts @@ -14,7 +14,7 @@ import { logger } from '../lib/logger.js'; import { getNameFromNameParts, isSingletonType } from '../lib/resource-path.js'; /** Keys of OverrideConfig that hold OverrideSection values (excludes non-section fields). */ -type OverrideSectionKey = keyof { [K in keyof OverrideConfig as OverrideConfig[K] extends OverrideSection | undefined ? K : never]: unknown }; +export type OverrideSectionKey = keyof { [K in keyof OverrideConfig as OverrideConfig[K] extends OverrideSection | undefined ? K : never]: unknown }; /** * Map resource types to their top-level override config section key. @@ -65,10 +65,16 @@ const GRANDCHILD_OVERRIDE_MAP: Partial key.toLowerCase() === lowerName ); } @@ -91,7 +97,7 @@ export function applyOverrides( // Try direct override lookup first const directSection = OVERRIDE_SECTION_MAP[descriptor.type]; if (directSection) { - const section = overrides[directSection]; + const section = resolveOverrideSection(overrides, directSection, descriptor.workspace); if (!section) return { ...json }; return applyFromSection(descriptor, json, section); } @@ -99,18 +105,46 @@ export function applyOverrides( // Try nested child override lookup const childMapping = CHILD_OVERRIDE_MAP[descriptor.type]; if (childMapping) { - return applyNestedOverride(descriptor, json, overrides, childMapping); + const parentSection = resolveOverrideSection(overrides, childMapping.parentSection, descriptor.workspace); + if (!parentSection) return { ...json }; + return applyNestedOverride(descriptor, json, parentSection, childMapping); } // Try grandchild (3-level) override lookup const grandchildMapping = GRANDCHILD_OVERRIDE_MAP[descriptor.type]; if (grandchildMapping) { - return applyGrandchildOverride(descriptor, json, overrides, grandchildMapping); + const parentSection = resolveOverrideSection(overrides, grandchildMapping.parentSection, descriptor.workspace); + if (!parentSection) return { ...json }; + return applyGrandchildOverride(descriptor, json, parentSection, grandchildMapping); } return { ...json }; } +/** + * Resolve the override section to search for a given section key, honoring workspace scoping. + * Workspace-scoped resources are strictly isolated: they only read from + * `overrides.workspaces[workspace].children[sectionKey]`, never the top-level section. + */ +export function resolveOverrideSection( + overrides: OverrideConfig | undefined, + sectionKey: OverrideSectionKey, + workspace: string | undefined +): OverrideSection | undefined { + if (!overrides) return undefined; + if (!workspace) { + return overrides[sectionKey]; + } + + const workspacesSection = overrides.workspaces; + if (!workspacesSection) return undefined; + + const workspaceEntry = findEntryByName(workspacesSection, workspace); + if (!workspaceEntry?.children) return undefined; + + return workspaceEntry.children[sectionKey]; +} + /** * Apply override from a direct section match. * For singleton resources (e.g., ServicePolicy with nameParts: []), @@ -172,12 +206,9 @@ function applyFromSection( function applyNestedOverride( descriptor: ResourceDescriptor, json: Record, - overrides: OverrideConfig, + parentSection: OverrideSection, mapping: { parentSection: OverrideSectionKey; childKey: string } ): Record { - const parentSection = overrides[mapping.parentSection]; - if (!parentSection) return { ...json }; - const parentName = descriptor.nameParts[0]; if (!parentName) return { ...json }; @@ -220,12 +251,9 @@ function applyNestedOverride( function applyGrandchildOverride( descriptor: ResourceDescriptor, json: Record, - overrides: OverrideConfig, + parentSection: OverrideSection, mapping: { parentSection: OverrideSectionKey; childKey: string; grandchildKey: string } ): Record { - const parentSection = overrides[mapping.parentSection]; - if (!parentSection) return { ...json }; - const parentName = descriptor.nameParts[0]; if (!parentName) return { ...json }; diff --git a/src/services/publish-service.ts b/src/services/publish-service.ts index 517667a9..404949c4 100644 --- a/src/services/publish-service.ts +++ b/src/services/publish-service.ts @@ -667,7 +667,7 @@ function splitNamedValues( for (const descriptor of descriptors) { if (descriptor.type === ResourceType.NamedValue) { const name = getNamePart(descriptor.nameParts, 0); - if (isAutoGeneratedId(name) && !hasNamedValueOverride(name, overrides)) { + if (isAutoGeneratedId(name) && !hasNamedValueOverride(name, overrides, descriptor.workspace)) { logger.debug(`Skipping auto-generated named value: ${name}`); continue; } diff --git a/src/services/resource-publisher.ts b/src/services/resource-publisher.ts index 9e3c1ef1..f876c175 100644 --- a/src/services/resource-publisher.ts +++ b/src/services/resource-publisher.ts @@ -16,7 +16,7 @@ import type { } from '../models/types.js'; import type { PublishConfig } from '../models/config.js'; import { ResourceType, RESOURCE_TYPE_METADATA, MANAGED_GATEWAY_NAME } from '../models/resource-types.js'; -import { applyOverrides } from './override-merger.js'; +import { applyOverrides, resolveOverrideSection } from './override-merger.js'; import { checkKeyVaultSecretAccess } from './keyvault-checker.js'; import { getNamePart, @@ -338,7 +338,7 @@ export async function publishResource( const hasDisplayNameOverride = hasExplicitPropertyOverride( canonicalName, 'displayName', - config.overrides?.namedValues, + resolveOverrideSection(config.overrides, 'namedValues', descriptor.workspace), ); if (!hasDisplayNameOverride) { const props = json.properties as Record | undefined; @@ -469,8 +469,9 @@ export async function publishResource( // validation errors in APIM's revision creation. if (descriptor.type === ResourceType.Api) { const apiName = getNamePart(descriptor.nameParts, 0); + const apiOverrides = resolveOverrideSection(config.overrides, 'apis', descriptor.workspace); json = normalizeApiAuthenticationSettings(json, { - preferLegacyFields: prefersLegacyAuthOverride(apiName, config.overrides?.apis), + preferLegacyFields: prefersLegacyAuthOverride(apiName, apiOverrides), }); if (apiName.includes(';rev=')) { const baseApiName = apiName.split(';rev=')[0]; @@ -498,7 +499,7 @@ export async function publishResource( } if ( Object.hasOwn(cleanProps, 'description') && - hasExplicitPropertyOverride(apiName, 'description', config.overrides?.apis) + hasExplicitPropertyOverride(apiName, 'description', apiOverrides) ) { logger.warn( `Ignoring 'description' override for revision '${apiName}': ` + @@ -1209,7 +1210,11 @@ export function applyApiPathPrefix( } const canonicalApiName = getNamePart(descriptor.nameParts, 0).split(';rev=')[0]; - if (hasExplicitPropertyOverride(canonicalApiName, 'path', config.overrides?.apis)) { + if (hasExplicitPropertyOverride( + canonicalApiName, + 'path', + resolveOverrideSection(config.overrides, 'apis', descriptor.workspace), + )) { return json; } @@ -1631,7 +1636,7 @@ async function rewriteNamedValueReferences( const hasDisplayNameOverride = hasExplicitPropertyOverride( ref, 'displayName', - overrides?.namedValues, + resolveOverrideSection(overrides, 'namedValues', descriptor.workspace), ); if (!hasDisplayNameOverride) { const baseDisplayName = displayName ?? ref; diff --git a/src/services/secret-redaction-guard.ts b/src/services/secret-redaction-guard.ts index ba1a8442..106a4817 100644 --- a/src/services/secret-redaction-guard.ts +++ b/src/services/secret-redaction-guard.ts @@ -111,7 +111,7 @@ async function scanNamedValue( // supplies an explicit override, so scanning them here would flag markers in // artifacts that are never sent to APIM. Mirror the publish filter exactly. const name = getNamePart(descriptor.nameParts, 0); - if (isAutoGeneratedId(name) && !hasNamedValueOverride(name, config.overrides)) { + if (isAutoGeneratedId(name) && !hasNamedValueOverride(name, config.overrides, descriptor.workspace)) { return undefined; } diff --git a/tests/unit/services/api-publisher.test.ts b/tests/unit/services/api-publisher.test.ts index 34460747..6e3592fa 100644 --- a/tests/unit/services/api-publisher.test.ts +++ b/tests/unit/services/api-publisher.test.ts @@ -25,10 +25,16 @@ vi.mock('../../../src/services/resource-publisher.js', async () => { }; }); -// Mock override-merger -vi.mock('../../../src/services/override-merger.js', () => ({ - applyOverrides: vi.fn((descriptor, json) => json), -})); +// Mock override-merger while retaining its workspace section resolver. +vi.mock('../../../src/services/override-merger.js', async () => { + const actual = await vi.importActual( + '../../../src/services/override-merger.js' + ); + return { + ...actual, + applyOverrides: vi.fn((_descriptor, json) => json), + }; +}); // Mock parallel-runner const mockRunParallel = vi.fn(); @@ -1446,6 +1452,63 @@ describe('api-publisher', () => { expect(client.putResource).toHaveBeenCalled(); }); + it('should honor workspace API auth and path overrides during root post-processing', async () => { + const client = createMockClient(); + const store = createMockStore(); + store.readResource.mockResolvedValue({ + name: 'orders-api', + properties: { + path: 'workspace/orders', + authenticationSettings: { + oAuth2: { authorizationServerId: 'workspace-server' }, + oAuth2AuthenticationSettings: [{ authorizationServerId: 'artifact-server' }], + }, + }, + }); + const descriptor: ResourceDescriptor = { + type: ResourceType.Api, + nameParts: ['orders-api'], + workspace: 'team-a', + }; + const config: PublishConfig = { + ...testConfig, + envMapping: { + prefix: 'dev-', + suffix: '', + apiPathPrefix: 'dev/', + appliesTo: DEFAULT_APPLIES_TO, + }, + overrides: { + workspaces: { + 'team-a': { + properties: {}, + children: { + apis: { + 'orders-api': { + properties: { + path: 'workspace/orders', + authenticationSettings: { + oAuth2: { authorizationServerId: 'workspace-server' }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + await publishApi(client, store, testContext, descriptor, config); + + const putJson = client.putResource.mock.calls[0]?.[2] as Record; + const properties = putJson.properties as Record; + expect(properties.path).toBe('workspace/orders'); + expect(properties.authenticationSettings).toEqual({ + oAuth2: { authorizationServerId: 'workspace-server' }, + }); + }); + it('should return failed result on exception', async () => { const client = createMockClient(); const store = createMockStore([]); diff --git a/tests/unit/services/override-merger.test.ts b/tests/unit/services/override-merger.test.ts index 13033d6d..a58d503a 100644 --- a/tests/unit/services/override-merger.test.ts +++ b/tests/unit/services/override-merger.test.ts @@ -655,4 +655,229 @@ describe('override-merger', () => { expect(result.properties).toHaveProperty('isCurrent', true); }); }); + + describe('workspace-scoped overrides', () => { + it('should apply a direct override for a workspace-scoped API', () => { + const descriptor: ResourceDescriptor = { + type: ResourceType.Api, + nameParts: ['my-api'], + workspace: 'ws1', + }; + const json = { name: 'my-api', properties: { serviceUrl: 'https://dev.example.com' } }; + const overrideConfig: OverrideConfig = { + workspaces: { + ws1: { + properties: {}, + children: { + apis: { + 'my-api': { + properties: { serviceUrl: 'https://prod.example.com' }, + }, + }, + }, + }, + }, + }; + + const result = applyOverrides(descriptor, json, overrideConfig); + expect(result.properties).toHaveProperty('serviceUrl', 'https://prod.example.com'); + }); + + it('should apply a direct override for a workspace-scoped backend', () => { + const descriptor: ResourceDescriptor = { + type: ResourceType.Backend, + nameParts: ['orders-backend'], + workspace: 'partner-workspace', + }; + const json = { name: 'orders-backend', properties: { url: 'https://orders-dev.example.com' } }; + const overrideConfig: OverrideConfig = { + workspaces: { + 'partner-workspace': { + properties: {}, + children: { + backends: { + 'orders-backend': { + properties: { url: 'https://orders-prod.example.com' }, + }, + }, + }, + }, + }, + }; + + const result = applyOverrides(descriptor, json, overrideConfig); + expect(result.properties).toHaveProperty('url', 'https://orders-prod.example.com'); + }); + + it('should use the override from the matching workspace when multiple workspaces define the same resource name', () => { + const descriptor: ResourceDescriptor = { + type: ResourceType.Api, + nameParts: ['my-api'], + workspace: 'ws2', + }; + const json = { name: 'my-api', properties: { serviceUrl: 'https://dev.example.com' } }; + const overrideConfig: OverrideConfig = { + workspaces: { + ws1: { + properties: {}, + children: { + apis: { + 'my-api': { + properties: { serviceUrl: 'https://ws1.example.com' }, + }, + }, + }, + }, + ws2: { + properties: {}, + children: { + apis: { + 'my-api': { + properties: { serviceUrl: 'https://ws2.example.com' }, + }, + }, + }, + }, + }, + }; + + const result = applyOverrides(descriptor, json, overrideConfig); + expect(result.properties).toHaveProperty('serviceUrl', 'https://ws2.example.com'); + }); + + it('should apply a nested override for a workspace-scoped API diagnostic', () => { + const descriptor: ResourceDescriptor = { + type: ResourceType.ApiDiagnostic, + nameParts: ['my-api', 'applicationinsights'], + workspace: 'ws1', + }; + const json = { name: 'applicationinsights', properties: { loggerId: '/old-logger' } }; + const overrideConfig: OverrideConfig = { + workspaces: { + ws1: { + properties: {}, + children: { + apis: { + 'my-api': { + properties: {}, + children: { + diagnostics: { + applicationinsights: { + properties: { loggerId: '/new-logger' }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const result = applyOverrides(descriptor, json, overrideConfig); + expect(result.properties).toHaveProperty('loggerId', '/new-logger'); + }); + + it('should apply a grandchild override for a workspace-scoped API operation policy', () => { + const descriptor: ResourceDescriptor = { + type: ResourceType.ApiOperationPolicy, + nameParts: ['my-api', 'get-pets'], + workspace: 'ws1', + }; + const json = { name: 'policy', properties: { format: 'xml' } }; + const overrideConfig: OverrideConfig = { + workspaces: { + ws1: { + properties: {}, + children: { + apis: { + 'my-api': { + properties: {}, + children: { + operations: { + 'get-pets': { + properties: {}, + children: { + policies: { + policy: { properties: { format: 'rawxml' } }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const result = applyOverrides(descriptor, json, overrideConfig); + expect(result.properties).toHaveProperty('format', 'rawxml'); + }); + + it('should not leak a top-level override into a workspace-scoped resource', () => { + const descriptor: ResourceDescriptor = { + type: ResourceType.Api, + nameParts: ['my-api'], + workspace: 'ws1', + }; + const json = { name: 'my-api', properties: { serviceUrl: 'https://dev.example.com' } }; + // Same resource name is overridden at the top level, but no workspace-scoped override exists. + const overrideConfig: OverrideConfig = { + apis: { + 'my-api': { + properties: { serviceUrl: 'https://should-not-apply.example.com' }, + }, + }, + }; + + const result = applyOverrides(descriptor, json, overrideConfig); + expect(result).toEqual(json); + }); + + it('should still apply workspace container property overrides (no workspace field on descriptor)', () => { + const descriptor: ResourceDescriptor = { + type: ResourceType.Workspace, + nameParts: ['ws1'], + }; + const json = { name: 'ws1', properties: { displayName: 'Dev Workspace' } }; + const overrideConfig: OverrideConfig = { + workspaces: { + ws1: { + properties: { displayName: 'Prod Workspace' }, + }, + }, + }; + + const result = applyOverrides(descriptor, json, overrideConfig); + expect(result.properties).toHaveProperty('displayName', 'Prod Workspace'); + }); + + it('should match workspace names case-insensitively', () => { + const descriptor: ResourceDescriptor = { + type: ResourceType.Api, + nameParts: ['my-api'], + workspace: 'ws1', + }; + const json = { name: 'my-api', properties: { serviceUrl: 'https://dev.example.com' } }; + const overrideConfig: OverrideConfig = { + workspaces: { + WS1: { + properties: {}, + children: { + apis: { + 'my-api': { + properties: { serviceUrl: 'https://prod.example.com' }, + }, + }, + }, + }, + }, + }; + + const result = applyOverrides(descriptor, json, overrideConfig); + expect(result.properties).toHaveProperty('serviceUrl', 'https://prod.example.com'); + }); + }); }); diff --git a/tests/unit/services/publish-service.test.ts b/tests/unit/services/publish-service.test.ts index 503c3bef..c710ddee 100644 --- a/tests/unit/services/publish-service.test.ts +++ b/tests/unit/services/publish-service.test.ts @@ -2089,5 +2089,82 @@ describe('publish-service', () => { expect(putNames).toContain('regular-nv'); expect(putNames).toContain(autoGenId); }); + + it('should publish workspace auto-generated named values when a workspace override exists', async () => { + const autoGenId = 'aabbccddeeff112233445566'; + const descriptor: ResourceDescriptor = { + type: ResourceType.NamedValue, + nameParts: [autoGenId], + workspace: 'team-a', + }; + const client = createMockClient(); + const store = createMockStore([descriptor]); + vi.mocked(store.readResource).mockResolvedValue({ + name: autoGenId, + properties: { value: 'source-key', secret: true }, + }); + + await runPublish(client, store, { + service: testContext, + sourceDir: '/source', + dryRun: false, + deleteUnmatched: false, + logLevel: LogLevel.INFO, + overrides: { + workspaces: { + 'team-a': { + properties: {}, + children: { + namedValues: { + [autoGenId]: { + properties: { value: 'workspace-key' }, + }, + }, + }, + }, + }, + }, + }); + + expect(client.putResource).toHaveBeenCalledWith( + testContext, + descriptor, + expect.objectContaining({ + properties: expect.objectContaining({ value: 'workspace-key' }), + }) + ); + }); + + it('should not publish a workspace auto-generated named value for a service-level-only override', async () => { + const autoGenId = 'aabbccddeeff112233445566'; + const descriptor: ResourceDescriptor = { + type: ResourceType.NamedValue, + nameParts: [autoGenId], + workspace: 'team-a', + }; + const client = createMockClient(); + const store = createMockStore([descriptor]); + vi.mocked(store.readResource).mockResolvedValue({ + name: autoGenId, + properties: { value: 'source-key', secret: true }, + }); + + await runPublish(client, store, { + service: testContext, + sourceDir: '/source', + dryRun: false, + deleteUnmatched: false, + logLevel: LogLevel.INFO, + overrides: { + namedValues: { + [autoGenId]: { + properties: { value: 'service-key' }, + }, + }, + }, + }); + + expect(client.putResource).not.toHaveBeenCalled(); + }); }); }); diff --git a/tests/unit/services/resource-publisher.test.ts b/tests/unit/services/resource-publisher.test.ts index 61c6078e..b1f30dba 100644 --- a/tests/unit/services/resource-publisher.test.ts +++ b/tests/unit/services/resource-publisher.test.ts @@ -324,6 +324,106 @@ describe('resource-publisher', () => { ); }); + it('should honor workspace API auth and path overrides during post-processing', async () => { + const client = createMockClient(); + const store = createMockStore(); + store.readResource.mockResolvedValue({ + name: 'orders-api', + properties: { + path: 'orders', + authenticationSettings: { + oAuth2: { authorizationServerId: 'workspace-server' }, + oAuth2AuthenticationSettings: [{ authorizationServerId: 'artifact-server' }], + }, + }, + }); + const descriptor: ResourceDescriptor = { + type: ResourceType.Api, + nameParts: ['orders-api'], + workspace: 'team-a', + }; + const config: PublishConfig = { + ...testConfig, + envMapping: { + prefix: 'dev-', + suffix: '', + apiPathPrefix: 'dev/', + appliesTo: new Set([ResourceType.Api]), + }, + overrides: { + workspaces: { + 'team-a': { + properties: {}, + children: { + apis: { + 'orders-api': { + properties: { + path: 'workspace/orders', + authenticationSettings: { + oAuth2: { authorizationServerId: 'workspace-server' }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + await publishResource(client, store, testContext, descriptor, config); + + const putJson = client.putResource.mock.calls[0]?.[2] as Record; + const properties = putJson.properties as Record; + expect(properties.path).toBe('workspace/orders'); + expect(properties.authenticationSettings).toEqual({ + oAuth2: { authorizationServerId: 'workspace-server' }, + }); + }); + + it('should preserve an explicit workspace named-value displayName with environment affixes', async () => { + const client = createMockClient(); + const store = createMockStore(); + store.readResource.mockResolvedValue({ + name: 'api-key', + properties: { displayName: 'api-key', value: 'test-value' }, + }); + const descriptor: ResourceDescriptor = { + type: ResourceType.NamedValue, + nameParts: ['api-key'], + workspace: 'team-a', + }; + const config: PublishConfig = { + ...testConfig, + envMapping: { + prefix: 'dev-', + suffix: '', + appliesTo: new Set([ResourceType.NamedValue]), + }, + overrides: { + workspaces: { + 'team-a': { + properties: {}, + children: { + namedValues: { + 'api-key': { + properties: { displayName: 'workspace-api-key' }, + }, + }, + }, + }, + }, + }, + }; + + await publishResource(client, store, testContext, descriptor, config); + + const putJson = client.putResource.mock.calls[0]?.[2] as Record; + const properties = putJson.properties as Record; + expect(properties.displayName).toBe('workspace-api-key'); + expect(client.putResource.mock.calls[0]?.[1].nameParts).toEqual(['dev-api-key']); + }); + it('should preserve opaque JSON properties', async () => { const client = createMockClient(); const store = createMockStore(); @@ -1934,6 +2034,63 @@ describe('resource-publisher', () => { expect(creds.instrumentationKey).toBe('{{Logger-Credentials--abc123}}'); }); + it('should keep an explicit workspace named-value displayName unprefixed in logger credentials', async () => { + const client = createMockClient(); + const store = createMockStore(); + const loggerJson = { + name: 'workspace-logger', + properties: { + loggerType: 'applicationInsights', + credentials: { instrumentationKey: '{{api-key}}' }, + }, + }; + store.readResource.mockImplementation(async (_dir: string, descriptor: ResourceDescriptor) => { + if (descriptor.type === ResourceType.Logger) return loggerJson; + if (descriptor.type === ResourceType.NamedValue && descriptor.nameParts[0] === 'api-key') { + return { + name: 'api-key', + properties: { displayName: 'api-key', secret: true }, + }; + } + return null; + }); + const descriptor: ResourceDescriptor = { + type: ResourceType.Logger, + nameParts: ['workspace-logger'], + workspace: 'team-a', + }; + const config: PublishConfig = { + ...testConfig, + envMapping: { + prefix: 'dev-', + suffix: '', + appliesTo: new Set([ResourceType.NamedValue]), + }, + overrides: { + workspaces: { + 'team-a': { + properties: {}, + children: { + namedValues: { + 'api-key': { + properties: { displayName: 'workspace-api-key' }, + }, + }, + }, + }, + }, + }, + }; + + const result = await publishResource(client, store, testContext, descriptor, config); + + expect(result.status).toBe('success'); + const putJson = client.putResource.mock.calls[0]?.[2] as Record; + const properties = putJson.properties as Record; + const credentials = properties.credentials as Record; + expect(credentials.instrumentationKey).toBe('{{workspace-api-key}}'); + }); + it('should not rewrite credentials for non-Logger resources', async () => { const client = createMockClient(); const store = createMockStore();