diff --git a/controlplane/test/feature-flag/feature-flag-integration.test.ts b/controlplane/test/feature-flag/feature-flag-integration.test.ts index 909ec1817c..ff0d4f9a76 100644 --- a/controlplane/test/feature-flag/feature-flag-integration.test.ts +++ b/controlplane/test/feature-flag/feature-flag-integration.test.ts @@ -9,6 +9,7 @@ import { assertExecutionConfigSubgraphNames, assertFeatureFlagExecutionConfig, assertNumberOfCompositions, + assertMapperContentIsCorrect, createAndPublishSubgraph, createFeatureFlag, createFederatedGraph, @@ -1508,6 +1509,8 @@ describe('Feature flag integration tests', () => { const ffKey = blobStorage.keys().at(-1); expect(ffKey).toContain(`${federatedGraphResponse.graph!.id}/manifest/feature-flags/${featureFlagName}.json`); + await assertMapperContentIsCorrect(blobStorage, 1); + // The base recomposition and the feature flag composition await assertNumberOfCompositions(client, baseGraphName, 2); @@ -1581,6 +1584,8 @@ describe('Feature flag integration tests', () => { const ffKey = blobStorage.keys().at(-1); expect(ffKey).toContain(`${federatedGraphResponse.graph!.id}/manifest/feature-flags/${featureFlagName}.json`); + await assertMapperContentIsCorrect(blobStorage, 1); + // The base recomposition and the feature flag composition await assertNumberOfCompositions(client, baseGraphName, 2, namespace); await assertFeatureFlagExecutionConfig(blobStorage, key, false); @@ -1655,6 +1660,8 @@ describe('Feature flag integration tests', () => { const ffKey = blobStorage.keys().at(-1); expect(ffKey).toContain(`${federatedGraphResponse.graph!.id}/manifest/feature-flags/${featureFlagName}.json`); + await assertMapperContentIsCorrect(blobStorage, 1); + // The feature flag is disabled again and trigger a base recomposition await toggleFeatureFlag(client, featureFlagName, false, namespace); await assertNumberOfCompositions(client, baseGraphName, 2, namespace); @@ -1750,6 +1757,8 @@ describe('Feature flag integration tests', () => { ]), ); + await assertMapperContentIsCorrect(blobStorage, 2); + // The base recomposition and the feature flag composition await assertNumberOfCompositions(client, baseGraphName, 2, namespace); await assertFeatureFlagExecutionConfig(blobStorage, baseGraphKey, false); @@ -1845,6 +1854,8 @@ describe('Feature flag integration tests', () => { ]), ); + await assertMapperContentIsCorrect(blobStorage, 2); + // The base recomposition and the feature flag composition await assertNumberOfCompositions(client, baseGraphName, 2, namespace); await assertFeatureFlagExecutionConfig(blobStorage, baseGraphKey, false); @@ -1967,6 +1978,8 @@ describe('Feature flag integration tests', () => { const ffKey = blobStorage.keys().at(-1); expect(ffKey).toContain(`${baseGraphResponse.graph!.id}/manifest/feature-flags/${featureFlagName}.json`); + await assertMapperContentIsCorrect(blobStorage, 1); + // There will be a failed base composition and one feature flag compositions await assertNumberOfCompositions(client, baseGraphName, 3, namespace); await createAndPublishSubgraph( @@ -2068,6 +2081,8 @@ describe('Feature flag integration tests', () => { ]), ); + await assertMapperContentIsCorrect(blobStorage, 2); + // The base recomposition and the feature flag composition await assertNumberOfCompositions(client, baseGraphName, 2, namespace); await assertFeatureFlagExecutionConfig(blobStorage, baseGraphKey, false); @@ -2187,6 +2202,8 @@ describe('Feature flag integration tests', () => { ]), ); + await assertMapperContentIsCorrect(blobStorage, 2); + // There should be a base recomposition, a feature flag composition, and an embedded feature flag config for (const { name, key } of graphNamesAndKeys) { await assertNumberOfCompositions(client, name, 2, namespace); @@ -2349,6 +2366,8 @@ describe('Feature flag integration tests', () => { ]), ); + await assertMapperContentIsCorrect(blobStorage, 4); + /* * Each federated graph should have produced two total compositions: * 1. The original base composition @@ -2398,6 +2417,8 @@ describe('Feature flag integration tests', () => { ]), ); + await assertMapperContentIsCorrect(blobStorage, 4); + const deleteFeatureSubgraphResponse = await client.deleteFederatedSubgraph({ subgraphName: 'products-feature', namespace, @@ -2503,6 +2524,8 @@ describe('Feature flag integration tests', () => { const ffKey = blobStorage.keys()[2]; expect(ffKey).toContain(`${federatedGraphResponse.graph!.id}/manifest/feature-flags/${featureFlagName}.json`); + await assertMapperContentIsCorrect(blobStorage, 1); + // The base recomposition and the feature flag composition await assertNumberOfCompositions(client, baseGraphName, 2); await assertFeatureFlagExecutionConfig(blobStorage, key, false); @@ -2625,6 +2648,8 @@ describe('Feature flag integration tests', () => { const ffKey = blobStorage.keys().at(-1); expect(ffKey).toContain(`${baseGraphResponse.graph!.id}/manifest/feature-flags/${featureFlagName}.json`); + await assertMapperContentIsCorrect(blobStorage, 1); + // The feature flag composition await assertNumberOfCompositions(client, baseGraphName, 3, namespace); await assertFeatureFlagExecutionConfig(blobStorage, baseGraphKey, false); @@ -2912,6 +2937,9 @@ describe('Feature flag integration tests', () => { ), ]), ); + + // Skip the number of feature flag check as we have two federated graphs with a different number of feature flags + await assertMapperContentIsCorrect(blobStorage, 2, -1); }, ); }); diff --git a/controlplane/test/test-util.ts b/controlplane/test/test-util.ts index 881ffc888f..dba75bfc2b 100644 --- a/controlplane/test/test-util.ts +++ b/controlplane/test/test-util.ts @@ -1,4 +1,4 @@ -import { randomUUID, UUID } from 'node:crypto'; +import { createHash, randomUUID, UUID } from 'node:crypto'; import { join, resolve } from 'node:path'; import fs from 'node:fs'; import { createPromiseClient, PromiseClient } from '@connectrpc/connect'; @@ -888,6 +888,60 @@ export async function assertExecutionConfigSubgraphNames( expect(subgraphIds.has(subgraph.id)).toBe(true); } } +export async function assertMapperContentIsCorrect( + blobStorage: InMemoryBlobStorage, + expectedMapperKeysCount: number, + expectedNumberOfFeatureFlags = 1, +) { + const blobKeys = blobStorage.keys(); + const existingMappers = blobKeys.filter((key) => key.endsWith('mapper.json')); + expect(existingMappers).toHaveLength(expectedMapperKeysCount); + + let numberOfRouterConfigsInBlobStorage = 0; + for (const mapperKey of existingMappers) { + const mapperBlob = await blobStorage.getObject({ key: mapperKey }); + const mapper = await mapperBlob.stream + .getReader() + .read() + .then((result) => JSON.parse(result.value.toString())); + + const keyPrefix = mapperKey.split('/').slice(0, -1).join('/'); + const mapperEntries = Object.entries(mapper); + + /** + * The mapper should always contain the number of expected feature flags plus one (the federated graph. + * + * If the expected number of feature flags is `-1`, we are skipping this check. + */ + if (expectedNumberOfFeatureFlags !== -1) { + expect(mapperEntries).toHaveLength(expectedNumberOfFeatureFlags + 1); + } + + for (const [featureFlagName, hash] of mapperEntries) { + const key = + featureFlagName === '' ? `${keyPrefix}/latest.json` : `${keyPrefix}/feature-flags/${featureFlagName}.json`; + + expect(blobKeys).include(key); + + const blob = await blobStorage.getObject({ key }); + const blobContent = await blob.stream + .getReader() + .read() + .then((result) => result.value.toString()); + + const actualHash = createHash('sha256').update(blobContent).digest('hex'); + expect(hash).toBe(actualHash); + + numberOfRouterConfigsInBlobStorage++; + } + } + + /** + * The number of elements in the blob storage should be the number of expected mappers plus the number + * of valid hashes in each mapper file + */ + expect(blobKeys).toHaveLength(numberOfRouterConfigsInBlobStorage + expectedMapperKeysCount); +} export async function toggleFeatureFlag( client: PromiseClient,