Skip to content

Commit f7931a8

Browse files
Add integration tests (#368)
* Add integration tests * fix lint * update * Migrate to azure pipeline (#372) * Migrate to azure pipeline * update * remove restriction for IT test * update
1 parent 774a2d3 commit f7931a8

9 files changed

Lines changed: 911 additions & 39 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 0 additions & 29 deletions
This file was deleted.

‎azure-pipelines.yml‎

Lines changed: 125 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,131 @@
11
trigger:
2-
- main
3-
- preview
4-
- release/*
2+
branches:
3+
include:
4+
- main
5+
- preview
6+
- release/*
57

68
pr:
7-
- main
8-
- preview
9-
- release/*
9+
branches:
10+
include:
11+
- main
12+
- preview
13+
- release/*
1014

1115
pool:
12-
vmImage: ubuntu-latest
16+
# The current Playwright version does not support Chromium on Ubuntu 26.04.
17+
vmImage: ubuntu-24.04
1318

14-
steps:
15-
- script: echo "Hello from Azure Pipelines"
16-
displayName: Hello world
19+
stages:
20+
- stage: Build
21+
displayName: Build and offline tests
22+
jobs:
23+
- job: NodeTests
24+
displayName: Node.js tests
25+
strategy:
26+
matrix:
27+
Node20:
28+
nodeVersion: 20.x
29+
Node22:
30+
nodeVersion: 22.x
31+
Node24:
32+
nodeVersion: 24.x
33+
steps:
34+
- checkout: self
35+
- task: UseNode@1
36+
displayName: Use Node.js $(nodeVersion)
37+
inputs:
38+
version: $(nodeVersion)
39+
- script: npm ci
40+
displayName: Install dependencies
41+
- script: npm run lint
42+
displayName: Lint
43+
- script: npm run build
44+
displayName: Build
45+
- script: npm test -- --reporter xunit --reporter-option "output=$(Agent.TempDirectory)/test-results/unit-tests.xml"
46+
displayName: Run Node.js unit tests
47+
- script: npm run test-browser -- --reporter=default --reporter=junit --outputFile="$(Agent.TempDirectory)/test-results/browser-tests.xml"
48+
displayName: Run browser tests
49+
- task: PublishTestResults@2
50+
displayName: Publish unit and browser test results
51+
condition: succeededOrFailed()
52+
inputs:
53+
testResultsFormat: JUnit
54+
testResultsFiles: |
55+
unit-tests.xml
56+
browser-tests.xml
57+
searchFolder: $(Agent.TempDirectory)/test-results
58+
mergeTestResults: true
59+
failTaskOnFailedTests: true
60+
failTaskOnFailureToPublishResults: true
61+
failTaskOnMissingResultsFile: true
62+
testRunTitle: Node.js $(nodeVersion) unit and browser tests
63+
64+
- stage: Integration
65+
displayName: Live integration tests
66+
dependsOn: Build
67+
jobs:
68+
- job: LiveTests
69+
displayName: Node.js live integration tests
70+
timeoutInMinutes: '15'
71+
steps:
72+
- checkout: self
73+
- task: UseNode@1
74+
displayName: Use Node.js 24
75+
inputs:
76+
version: 24.x
77+
- script: npm ci
78+
displayName: Install dependencies
79+
- task: AzureCLI@2
80+
displayName: Run integration tests with the service principal
81+
inputs:
82+
azureSubscription: 'JS Provider GitHub PR Integration Test'
83+
addSpnToEnvironment: true
84+
visibleAzLogin: false
85+
scriptType: pscore
86+
scriptLocation: inlineScript
87+
workingDirectory: $(Build.SourcesDirectory)
88+
inlineScript: |
89+
foreach ($name in @("servicePrincipalId", "tenantId", "idToken")) {
90+
if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) {
91+
throw "Missing $name. Configure the service connection with workload identity federation."
92+
}
93+
}
94+
if ($env:AZURE_CLIENT_SECRET -or $env:AZURE_CLIENT_CERTIFICATE_PATH) {
95+
throw "Do not configure client secrets or certificates for this federated test job."
96+
}
97+
98+
$env:AZURE_CLIENT_ID = $env:servicePrincipalId
99+
$env:AZURE_TENANT_ID = $env:tenantId
100+
$tokenPath = Join-Path $env:AGENT_TEMPDIRECTORY "appconfig-federated-$([Guid]::NewGuid().ToString('N')).txt"
101+
102+
try {
103+
Set-Content -LiteralPath $tokenPath -Value $env:idToken -NoNewline -Encoding utf8
104+
$env:AZURE_FEDERATED_TOKEN_FILE = $tokenPath
105+
$env:AZURE_SUBSCRIPTION_ID = az account show --query id --output tsv --only-show-errors
106+
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($env:AZURE_SUBSCRIPTION_ID)) {
107+
throw "Could not read the subscription selected by the service connection."
108+
}
109+
110+
$resultsPath = Join-Path $env:AGENT_TEMPDIRECTORY "test-results/integration-tests.xml"
111+
npm run test:integration -- --reporter xunit --reporter-option "output=$resultsPath"
112+
if ($LASTEXITCODE -ne 0) {
113+
throw "Integration tests failed with exit code $LASTEXITCODE."
114+
}
115+
} finally {
116+
$env:AZURE_FEDERATED_TOKEN_FILE = $null
117+
if (Test-Path -LiteralPath $tokenPath) {
118+
Remove-Item -LiteralPath $tokenPath -Force
119+
}
120+
}
121+
- task: PublishTestResults@2
122+
displayName: Publish integration test results
123+
condition: succeededOrFailed()
124+
inputs:
125+
testResultsFormat: JUnit
126+
testResultsFiles: integration-tests.xml
127+
searchFolder: $(Agent.TempDirectory)/test-results
128+
failTaskOnFailedTests: true
129+
failTaskOnFailureToPublishResults: true
130+
failTaskOnMissingResultsFile: true
131+
testRunTitle: Node.js 24 live integration tests

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"lint": "eslint src/ test/ examples/ --ext .js,.ts,.mjs",
3333
"fix-lint": "eslint src/ test/ examples/ --fix --ext .js,.ts,.mjs",
3434
"test": "mocha out/esm/test/*.test.js out/commonjs/test/*.test.js --parallel --timeout 100000",
35+
"test:integration": "npm run build-test-esm && mocha \"out/esm/test/integration/*.test.js\" --no-parallel --timeout 120000",
3536
"test-browser": "npx playwright install chromium && vitest --config vitest.browser.config.ts run"
3637
},
3738
"repository": {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { setTimeout as delay } from "node:timers/promises";
5+
import { assert } from "chai";
6+
import { DefaultAzureCredential } from "@azure/identity";
7+
import { load } from "../../src/index.js";
8+
import type { AzureAppConfiguration } from "../../src/index.js";
9+
import { createNotModifiedResponseTracker, INTEGRATION_TEST_ENDPOINT as endpoint, IntegrationTestContext, REFRESH_INTERVAL_IN_MS, refreshUntil } from "./helpers.js";
10+
11+
interface FeatureFlag {
12+
id: string;
13+
enabled: boolean;
14+
conditions: {
15+
client_filters: unknown[];
16+
requirement_type: "Any" | "All";
17+
};
18+
}
19+
20+
function getFeatureFlags(settings: AzureAppConfiguration): FeatureFlag[] {
21+
const featureManagement = settings.get<{ feature_flags: FeatureFlag[] }>("feature_management");
22+
if (featureManagement === undefined) {
23+
throw new Error("Expected the provider to return feature_management.feature_flags.");
24+
}
25+
assert.hasAllKeys(featureManagement, ["feature_flags"]);
26+
assert.isArray(featureManagement.feature_flags);
27+
return featureManagement.feature_flags;
28+
}
29+
30+
describe("integration: feature flags", function () {
31+
let credential: DefaultAzureCredential;
32+
let context: IntegrationTestContext;
33+
34+
before(() => {
35+
credential = new DefaultAzureCredential();
36+
});
37+
38+
beforeEach(() => {
39+
context = new IntegrationTestContext(endpoint, credential);
40+
console.info(`Integration test key prefix: ${context.keyPrefix}`);
41+
});
42+
43+
afterEach(async () => {
44+
await context.cleanup();
45+
});
46+
47+
it("loads only flags matching the selected IDs and label", async () => {
48+
await context.setSetting("message", "Ordinary value");
49+
await context.setFeatureFlag("selected-enabled", true, { label: "production" });
50+
await context.setFeatureFlag("selected-enabled", false, { label: "development" });
51+
await context.setFeatureFlag("selected-disabled", false, { label: "production" });
52+
await context.setFeatureFlag("excluded", true, { label: "production" });
53+
54+
const settings = await load(endpoint, credential, {
55+
selectors: [{ keyFilter: context.key("*") }],
56+
featureFlagOptions: {
57+
enabled: true,
58+
selectors: [{ keyFilter: context.key("selected-*"), labelFilter: "production" }]
59+
}
60+
});
61+
62+
assert.sameDeepMembers(getFeatureFlags(settings), [
63+
{
64+
id: context.key("selected-enabled"),
65+
enabled: true,
66+
conditions: { client_filters: [], requirement_type: "Any" }
67+
},
68+
{
69+
id: context.key("selected-disabled"),
70+
enabled: false,
71+
conditions: { client_filters: [], requirement_type: "Any" }
72+
}
73+
]);
74+
assert.equal(settings.get(context.key("message")), "Ordinary value");
75+
assert.sameMembers([...settings.keys()], [context.key("message"), "feature_management"]);
76+
});
77+
78+
it("refreshes changed, added, and deleted flags independently of ordinary settings", async () => {
79+
await context.setSetting("message", "Initial value");
80+
await context.setFeatureFlag("updated", true);
81+
await context.setFeatureFlag("removed", false);
82+
const notModified = createNotModifiedResponseTracker();
83+
const settings = await load(endpoint, credential, {
84+
selectors: [{ keyFilter: context.key("*") }],
85+
clientOptions: {
86+
additionalPolicies: [{ policy: notModified.policy, position: "perCall" }]
87+
},
88+
featureFlagOptions: {
89+
enabled: true,
90+
selectors: [{ keyFilter: context.key("*") }],
91+
refresh: { enabled: true, refreshIntervalInMs: REFRESH_INTERVAL_IN_MS }
92+
}
93+
});
94+
assert.sameDeepMembers(getFeatureFlags(settings), [
95+
{
96+
id: context.key("updated"),
97+
enabled: true,
98+
conditions: { client_filters: [], requirement_type: "Any" }
99+
},
100+
{
101+
id: context.key("removed"),
102+
enabled: false,
103+
conditions: { client_filters: [], requirement_type: "Any" }
104+
}
105+
]);
106+
assert.equal(settings.get(context.key("message")), "Initial value");
107+
108+
let notifications = 0;
109+
const listener = settings.onRefresh(() => notifications++);
110+
try {
111+
await context.setSetting("message", "Updated ordinary value");
112+
await context.setFeatureFlag("updated", false);
113+
await context.setFeatureFlag("added", true);
114+
await context.deleteFeatureFlag("removed");
115+
116+
await refreshUntil(settings, () => {
117+
const flags = getFeatureFlags(settings);
118+
return flags.some(flag => flag.id === context.key("updated") && !flag.enabled) &&
119+
flags.some(flag => flag.id === context.key("added") && flag.enabled) &&
120+
!flags.some(flag => flag.id === context.key("removed"));
121+
}, "feature flag updates, additions, and deletions");
122+
123+
assert.sameDeepMembers(getFeatureFlags(settings), [
124+
{
125+
id: context.key("updated"),
126+
enabled: false,
127+
conditions: { client_filters: [], requirement_type: "Any" }
128+
},
129+
{
130+
id: context.key("added"),
131+
enabled: true,
132+
conditions: { client_filters: [], requirement_type: "Any" }
133+
}
134+
]);
135+
assert.equal(settings.get(context.key("message")), "Initial value");
136+
assert.equal(notifications, 1);
137+
138+
const previousNotModifiedCount = notModified.count;
139+
await delay(REFRESH_INTERVAL_IN_MS);
140+
await settings.refresh();
141+
assert.isAbove(notModified.count, previousNotModifiedCount);
142+
assert.equal(notifications, 1);
143+
} finally {
144+
listener.dispose();
145+
}
146+
});
147+
});

0 commit comments

Comments
 (0)