Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,22 @@
"typescript.enablePromptUseWorkspaceTsdk": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "oxc.oxc-vscode",
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[json]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"oxc.enable": true,
"oxc.typeAware": true,
"oxc.path.oxlint": "scripts/oxlint-vscode",
"oxc.path.oxfmt": "scripts/oxfmt-vscode",
"oxc.path.tsgolint": "scripts/oxlint-tsgolint-vscode",
"eslint.enable": false
}
11 changes: 9 additions & 2 deletions packages/cli/src/utils/environment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { environmentIdentityById } from "@allurereport/core";
import { environmentIdentityById, validateAllowedEnvironmentId } from "@allurereport/core";
import type { FullConfig } from "@allurereport/core";
import { validateEnvironmentId, validateEnvironmentName } from "@allurereport/core-api";
import { Option, UsageError } from "clipanion";
Expand Down Expand Up @@ -55,7 +55,7 @@ const resolveEnvironmentByName = (
};

export const resolveCommandEnvironment = (
config: Pick<FullConfig, "environmentName" | "environmentId" | "environments">,
config: Pick<FullConfig, "environmentName" | "environmentId" | "environments" | "allowedEnvironments">,
options: CommandEnvironmentOptions & { source?: string },
) => {
const source = options.source ?? "cli";
Expand All @@ -74,6 +74,13 @@ export const resolveCommandEnvironment = (
}

const identity = identityFromId ?? identityFromName;
const allowedEnvironmentIds = new Set(config.allowedEnvironments ?? []);
const allowlistError =
identity?.id !== undefined ? validateAllowedEnvironmentId(identity.id, allowedEnvironmentIds, "cli") : undefined;

if (allowlistError) {
throw new UsageError(allowlistError);
}

return {
environmentId: identity?.id ?? config.environmentId,
Expand Down
34 changes: 33 additions & 1 deletion packages/cli/test/utils/environment.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { UsageError } from "clipanion";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";

vi.mock("@allurereport/core", async (importOriginal) => ({
...(await importOriginal<typeof import("@allurereport/core")>()),
validateAllowedEnvironmentId: (environmentId: string, allowedIds: ReadonlySet<string>, source: string) =>
allowedIds.size === 0 || allowedIds.has(environmentId)
? undefined
: `${source}: environment id ${JSON.stringify(environmentId)} is not listed in allowedEnvironments`,
}));

import { resolveCommandEnvironment } from "../../src/utils/environment.js";

Expand Down Expand Up @@ -68,4 +76,28 @@ describe("resolveCommandEnvironment", () => {
),
).toThrow(UsageError);
});

it("should fail when environment id is outside allowedEnvironments", () => {
expect(() =>
resolveCommandEnvironment(
{
...config,
allowedEnvironments: ["qa"],
},
{ environment: "prod_env" },
),
).toThrow('cli: environment id "prod_env" is not listed in allowedEnvironments');
});

it("should fail when environment name resolves to id outside allowedEnvironments", () => {
expect(() =>
resolveCommandEnvironment(
{
...config,
allowedEnvironments: ["qa"],
},
{ environmentName: "Production" },
),
).toThrow('cli: environment id "prod_env" is not listed in allowedEnvironments');
});
});
1 change: 1 addition & 0 deletions packages/core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface FullConfig {
* Environment display name which will be assigned to all tests.
*/
environmentName?: string;
allowedEnvironments?: string[];
environments?: EnvironmentsConfig;
environmentConfigInfo?: EnvironmentConfigInfo;
variables?: ReportVariables;
Expand Down
80 changes: 78 additions & 2 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import { readFile, stat } from "node:fs/promises";
import { extname, resolve } from "node:path";
import * as process from "node:process";

import { validateEnvironmentId } from "@allurereport/core-api";
import type { Config, PluginDescriptor } from "@allurereport/plugin-api";
import { parse } from "yaml";

import type { FullConfig, PluginInstance } from "./api.js";
import { readKnownIssues } from "./known.js";
import { FileSystemReportFiles } from "./plugin.js";
import { normalizeEnvironmentDescriptorMap, resolveEnvironmentIdentity } from "./utils/environment.js";
import {
normalizeAllowedEnvironmentIds,
normalizeEnvironmentDescriptorMap,
resolveEnvironmentIdentity,
validateAllowedEnvironmentId,
} from "./utils/environment.js";
import { importWrapper } from "./utils/module.js";
import { normalizeImportPath } from "./utils/path.js";

Expand Down Expand Up @@ -100,6 +106,7 @@ export const validateConfig = (config: Config) => {
"environment",
"environmentId",
"environmentName",
"allowedEnvironments",
"environments",
"appendHistory",
"qualityGate",
Expand Down Expand Up @@ -164,6 +171,13 @@ export const loadJsConfig = async (configPath: string): Promise<Config> => {

const resolveConfigEnvironments = (config: Config) => {
const errors: string[] = [];
const {
normalized: normalizedAllowedEnvironments,
normalizedSet: allowedEnvironmentIds,
errors: allowedEnvironmentErrors,
} = normalizeAllowedEnvironmentIds(config.allowedEnvironments, "config.allowedEnvironments");

errors.push(...allowedEnvironmentErrors);
const {
normalized: environments,
configInfo: environmentConfigInfo,
Expand All @@ -180,6 +194,65 @@ const resolveConfigEnvironments = (config: Config) => {
);
errors.push(...environmentErrors, ...forcedEnvironment.errors);

Object.keys(environments).forEach((environmentId) => {
const error = validateAllowedEnvironmentId(environmentId, allowedEnvironmentIds, "config.environments");

if (error) {
errors.push(error);
}
});

if (forcedEnvironment.identity?.id !== undefined) {
const error = validateAllowedEnvironmentId(forcedEnvironment.identity.id, allowedEnvironmentIds, "config");

if (error) {
errors.push(error);
}
}

for (const [rulesetIndex, ruleset] of (config.qualityGate?.rules ?? []).entries()) {
if (ruleset.allTestsContainEnv !== undefined) {
const validation = validateEnvironmentId(ruleset.allTestsContainEnv);

if (!validation.valid) {
errors.push(`config.qualityGate.rules[${rulesetIndex}].allTestsContainEnv: id ${validation.reason}`);
} else {
const error = validateAllowedEnvironmentId(
validation.normalized,
allowedEnvironmentIds,
`config.qualityGate.rules[${rulesetIndex}].allTestsContainEnv`,
);

if (error) {
errors.push(error);
}
}
}

if (ruleset.environmentsTested !== undefined) {
for (const [environmentIndex, environmentId] of ruleset.environmentsTested.entries()) {
const validation = validateEnvironmentId(environmentId);

if (!validation.valid) {
errors.push(
`config.qualityGate.rules[${rulesetIndex}].environmentsTested[${environmentIndex}]: id ${validation.reason}`,
);
continue;
}

const error = validateAllowedEnvironmentId(
validation.normalized,
allowedEnvironmentIds,
`config.qualityGate.rules[${rulesetIndex}].environmentsTested[${environmentIndex}]`,
);

if (error) {
errors.push(error);
}
}
}
}

if (errors.length > 0) {
throw new Error(`The provided Allure config contains invalid environments: ${errors.join("; ")}`);
}
Expand All @@ -189,6 +262,7 @@ const resolveConfigEnvironments = (config: Config) => {
environmentConfigInfo,
environmentId: forcedEnvironment.identity?.id,
environmentName: forcedEnvironment.identity?.name,
allowedEnvironments: normalizedAllowedEnvironments,
};
};

Expand All @@ -199,7 +273,8 @@ export const resolveConfig = async (config: Config, override: ConfigOverride = {
throw new Error(`The provided Allure config contains unsupported fields: ${validationResult.fields.join(", ")}`);
}

const { environments, environmentConfigInfo, environmentId, environmentName } = resolveConfigEnvironments(config);
const { environments, environmentConfigInfo, environmentId, environmentName, allowedEnvironments } =
resolveConfigEnvironments(config);

const name = override.name ?? config.name ?? "Allure Report";
const open = override.open ?? config.open ?? false;
Expand Down Expand Up @@ -230,6 +305,7 @@ export const resolveConfig = async (config: Config, override: ConfigOverride = {
known,
environmentId,
environmentName,
allowedEnvironments,
variables,
environments,
environmentConfigInfo,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/qualityGate/qualityGate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export class QualityGate {
},
expected,
knownIssues,
environmentId: environmentIdentity.id,
environmentName: environmentIdentity.name,
});

Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/qualityGate/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ export const maxDurationRule: QualityGateRule<number> = {
};

/**
* Fails if any test in the run does not have the given environment.
* Expected: environment name (string).
* Fails if any test in the run does not have the given environment ID.
* Expected: environment ID (string).
*/
export const allTestsContainEnvRule: QualityGateRule<string> = {
rule: "allTestsContainEnv",
message: ({ actual, expected }) =>
`Not all tests contain the required environment, ${bold(`"${expected}"`)}; ${bold(actual.length === 1 ? "one" : String(actual))} ${actual.length === 1 ? "test has" : "tests have"} different or missing environment`,
`Not all tests contain the required environment ID, ${bold(`"${expected}"`)}; ${bold(actual.length === 1 ? "one" : String(actual))} ${actual.length === 1 ? "test has" : "tests have"} different or missing environment`,
validate: async ({ trs, expected }) => {
const testsWithoutEnv = trs.filter((tr) => (tr.environment ?? "") !== expected);
const testsWithoutEnv = trs.filter((tr) => (tr.environmentId ?? "") !== expected);
const actual = testsWithoutEnv.length;

return {
Expand All @@ -88,16 +88,16 @@ export const allTestsContainEnvRule: QualityGateRule<string> = {
};

/**
* Fails if the run does not contain at least one test for each of the given environments.
* Expected: array of environment names (string[]).
* Fails if the run does not contain at least one test for each of the given environment IDs.
* Expected: array of environment IDs (string[]).
*/
export const environmentsTestedRule: QualityGateRule<string[]> = {
rule: "environmentsTested",
message: ({ actual, expected }) =>
`The following environments were not tested: "${actual.join('", "')}"; expected all of: "${expected.join('", "')}"`,
validate: async ({ trs, expected, state }) => {
const previouslyTested = new Set(state.getResult() ?? []);
const batchTested = trs.map((tr) => tr.environment).filter((env): env is string => env != null && env !== "");
const batchTested = trs.map((tr) => tr.environmentId).filter((env): env is string => env != null && env !== "");

const testedEnvs = new Set([...previouslyTested, ...batchTested]);

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export class AllureReport {
variables = {},
environmentId,
environmentName,
allowedEnvironments,
environments,
environmentConfigInfo,
output,
Expand Down Expand Up @@ -141,6 +142,7 @@ export class AllureReport {
defaultLabels,
environmentId,
environmentName,
allowedEnvironments,
});
this.#readers = [...readers];
this.#plugins = [...plugins];
Expand Down
Loading
Loading