@pagopa/dx-cli 0.22.4 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/adapters/commander/commands/add.d.ts +2 -6
- package/dist/adapters/commander/commands/add.js +4 -8
- package/dist/adapters/commander/commands/init.d.ts +2 -6
- package/dist/adapters/commander/commands/init.js +6 -5
- package/dist/adapters/commander/commands/savemoney.d.ts +11 -0
- package/dist/adapters/commander/commands/savemoney.js +30 -5
- package/dist/adapters/commander/commands/spec.d.ts +10 -0
- package/dist/adapters/commander/commands/spec.js +13 -0
- package/dist/adapters/commander/index.js +6 -2
- package/dist/adapters/commander/spec.d.ts +16 -0
- package/dist/adapters/commander/spec.js +54 -0
- package/dist/adapters/plop/generators/environment/actions.js +3 -3
- package/dist/adapters/plop/index.js +3 -5
- package/dist/adapters/plop/templates-path.d.ts +5 -0
- package/dist/adapters/plop/templates-path.js +6 -0
- package/dist/domain/dependencies.d.ts +15 -2
- package/dist/domain/spec.d.ts +50 -0
- package/dist/domain/spec.js +8 -0
- package/dist/index.js +15 -12
- package/package.json +5 -5
- package/templates/monorepo/nx.json +0 -1
- package/dist/adapters/azure/__tests__/cloud-account-repository.test.d.ts +0 -1
- package/dist/adapters/azure/__tests__/cloud-account-repository.test.js +0 -95
- package/dist/adapters/azure/__tests__/cloud-account-service.test.d.ts +0 -1
- package/dist/adapters/azure/__tests__/cloud-account-service.test.js +0 -378
- package/dist/adapters/codemods/__tests__/registry.test.d.ts +0 -1
- package/dist/adapters/codemods/__tests__/registry.test.js +0 -56
- package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.d.ts +0 -1
- package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.js +0 -77
- package/dist/adapters/codemods/__tests__/use-pnpm.test.d.ts +0 -1
- package/dist/adapters/codemods/__tests__/use-pnpm.test.js +0 -148
- package/dist/adapters/commander/__tests__/env.test.d.ts +0 -1
- package/dist/adapters/commander/__tests__/env.test.js +0 -45
- package/dist/adapters/commander/__tests__/error-reporting.test.d.ts +0 -1
- package/dist/adapters/commander/__tests__/error-reporting.test.js +0 -63
- package/dist/adapters/commander/__tests__/exit-with-error.test.d.ts +0 -1
- package/dist/adapters/commander/__tests__/exit-with-error.test.js +0 -92
- package/dist/adapters/commander/__tests__/global-options.test.d.ts +0 -1
- package/dist/adapters/commander/__tests__/global-options.test.js +0 -33
- package/dist/adapters/commander/commands/__tests__/add.test.d.ts +0 -4
- package/dist/adapters/commander/commands/__tests__/add.test.js +0 -167
- package/dist/adapters/commander/commands/__tests__/init.test.d.ts +0 -4
- package/dist/adapters/commander/commands/__tests__/init.test.js +0 -48
- package/dist/adapters/commander/commands/__tests__/preconditions.test.d.ts +0 -1
- package/dist/adapters/commander/commands/__tests__/preconditions.test.js +0 -32
- package/dist/adapters/commander/presenters/__tests__/index.test.d.ts +0 -1
- package/dist/adapters/commander/presenters/__tests__/index.test.js +0 -23
- package/dist/adapters/commander/presenters/__tests__/json.test.d.ts +0 -1
- package/dist/adapters/commander/presenters/__tests__/json.test.js +0 -108
- package/dist/adapters/commander/presenters/__tests__/text.test.d.ts +0 -1
- package/dist/adapters/commander/presenters/__tests__/text.test.js +0 -60
- package/dist/adapters/github/__tests__/github-repo.spec.d.ts +0 -1
- package/dist/adapters/github/__tests__/github-repo.spec.js +0 -67
- package/dist/adapters/node/__tests__/data.d.ts +0 -18
- package/dist/adapters/node/__tests__/data.js +0 -22
- package/dist/adapters/node/__tests__/package-json.test.d.ts +0 -1
- package/dist/adapters/node/__tests__/package-json.test.js +0 -86
- package/dist/adapters/node/__tests__/repository.test.d.ts +0 -1
- package/dist/adapters/node/__tests__/repository.test.js +0 -77
- package/dist/adapters/node/fs/__tests__/file-reader.test.d.ts +0 -1
- package/dist/adapters/node/fs/__tests__/file-reader.test.js +0 -80
- package/dist/adapters/node/json/__tests__/index.test.d.ts +0 -1
- package/dist/adapters/node/json/__tests__/index.test.js +0 -14
- package/dist/adapters/octokit/__tests__/index.test.d.ts +0 -1
- package/dist/adapters/octokit/__tests__/index.test.js +0 -414
- package/dist/adapters/pagopa-technology/__tests__/authorization.test.d.ts +0 -4
- package/dist/adapters/pagopa-technology/__tests__/authorization.test.js +0 -548
- package/dist/adapters/plop/__tests__/run-actions.test.d.ts +0 -1
- package/dist/adapters/plop/__tests__/run-actions.test.js +0 -68
- package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.d.ts +0 -1
- package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +0 -171
- package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.d.ts +0 -1
- package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.js +0 -134
- package/dist/adapters/plop/generators/environment/__tests__/actions.test.d.ts +0 -2
- package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +0 -92
- package/dist/adapters/plop/generators/environment/__tests__/prompts.test.d.ts +0 -1
- package/dist/adapters/plop/generators/environment/__tests__/prompts.test.js +0 -182
- package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.d.ts +0 -1
- package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.js +0 -113
- package/dist/adapters/plop/helpers/__tests__/terraform-state-key.test.d.ts +0 -1
- package/dist/adapters/plop/helpers/__tests__/terraform-state-key.test.js +0 -35
- package/dist/adapters/plop/helpers/__tests__/validate-prompt.test.d.ts +0 -1
- package/dist/adapters/plop/helpers/__tests__/validate-prompt.test.js +0 -60
- package/dist/adapters/yaml/__tests__/index.test.d.ts +0 -1
- package/dist/adapters/yaml/__tests__/index.test.js +0 -53
- package/dist/domain/__tests__/data.d.ts +0 -19
- package/dist/domain/__tests__/data.js +0 -28
- package/dist/domain/__tests__/environment.test.d.ts +0 -1
- package/dist/domain/__tests__/environment.test.js +0 -332
- package/dist/domain/__tests__/info.test.d.ts +0 -1
- package/dist/domain/__tests__/info.test.js +0 -77
- package/dist/domain/__tests__/package-json.test.d.ts +0 -1
- package/dist/domain/__tests__/package-json.test.js +0 -39
- package/dist/domain/__tests__/repository.test.d.ts +0 -1
- package/dist/domain/__tests__/repository.test.js +0 -111
- package/dist/domain/__tests__/workspace.test.d.ts +0 -1
- package/dist/domain/__tests__/workspace.test.js +0 -57
- package/dist/use-cases/__tests__/apply-codemod.test.d.ts +0 -1
- package/dist/use-cases/__tests__/apply-codemod.test.js +0 -71
- package/dist/use-cases/__tests__/list-codemods.test.d.ts +0 -1
- package/dist/use-cases/__tests__/list-codemods.test.js +0 -37
- package/dist/use-cases/__tests__/request-authorization.test.d.ts +0 -4
- package/dist/use-cases/__tests__/request-authorization.test.js +0 -43
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
-
import YAML from "yaml";
|
|
3
|
-
import { extractPackageExtensions, NPM, preparePackageJsonForPnpm, usePnpm, writePnpmWorkspaceFile, Yarn, } from "../use-pnpm.js";
|
|
4
|
-
const { readFile, stdout, writeFile } = vi.hoisted(() => ({
|
|
5
|
-
readFile: vi.fn(async () => ""),
|
|
6
|
-
stdout: vi.fn(),
|
|
7
|
-
writeFile: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
vi.mock("execa", () => ({
|
|
10
|
-
$: vi.fn((opts) => {
|
|
11
|
-
const hasOptions = typeof opts === "object" && Object.hasOwn(opts, "lines");
|
|
12
|
-
if (hasOptions) {
|
|
13
|
-
return () => stdout().then((output) => ({
|
|
14
|
-
stdout: output.split("\n"),
|
|
15
|
-
}));
|
|
16
|
-
}
|
|
17
|
-
return stdout().then((output) => ({
|
|
18
|
-
stdout: output,
|
|
19
|
-
}));
|
|
20
|
-
}),
|
|
21
|
-
}));
|
|
22
|
-
vi.mock("node:fs/promises", () => ({
|
|
23
|
-
default: {
|
|
24
|
-
appendFile: () => Promise.resolve(),
|
|
25
|
-
readFile,
|
|
26
|
-
rm: () => Promise.resolve(),
|
|
27
|
-
stat: () => Promise.resolve({
|
|
28
|
-
isFile: () => true,
|
|
29
|
-
}),
|
|
30
|
-
writeFile,
|
|
31
|
-
},
|
|
32
|
-
}));
|
|
33
|
-
vi.mock("../git.js", async () => ({
|
|
34
|
-
getLatestCommitShaOrRef: async () => "dummy-sha",
|
|
35
|
-
}));
|
|
36
|
-
describe("pm", () => {
|
|
37
|
-
test("yarn workspaces are parsed correctly", async () => {
|
|
38
|
-
const yarn = new Yarn();
|
|
39
|
-
stdout.mockResolvedValueOnce(`{"location":".","name":"io-messages"}\n{"location":"apps/citizen-func","name":"citizen-func"}`);
|
|
40
|
-
const workspaces = await yarn.listWorkspaces();
|
|
41
|
-
expect(workspaces).toEqual(["io-messages", "citizen-func"]);
|
|
42
|
-
});
|
|
43
|
-
test("npm workspaces are parsed correctly", async () => {
|
|
44
|
-
const npm = new NPM();
|
|
45
|
-
stdout.mockResolvedValueOnce(`[{ "name": "io-messages", "location": "." },{ "name": "citizen-func", "location": "apps/citizen-func" }]`);
|
|
46
|
-
const workspaces = await npm.listWorkspaces();
|
|
47
|
-
expect(workspaces).toEqual(["io-messages", "citizen-func"]);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
describe("extractPackageExtensions", () => {
|
|
51
|
-
beforeEach(() => {
|
|
52
|
-
vi.clearAllMocks();
|
|
53
|
-
});
|
|
54
|
-
test("returns packageExtensions only if present", async () => {
|
|
55
|
-
const yarnrc = {
|
|
56
|
-
packageExtensions: {
|
|
57
|
-
"some-package@*": {
|
|
58
|
-
peerDependencies: {
|
|
59
|
-
react: "*",
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
readFile.mockResolvedValueOnce(YAML.stringify(yarnrc));
|
|
65
|
-
await expect(extractPackageExtensions()).resolves.toEqual(yarnrc.packageExtensions);
|
|
66
|
-
expect(readFile).toHaveBeenCalledOnce();
|
|
67
|
-
});
|
|
68
|
-
test("returns undefined if packageExtensions is not present", async () => {
|
|
69
|
-
const yarnrc = {
|
|
70
|
-
someOtherField: {},
|
|
71
|
-
};
|
|
72
|
-
readFile.mockResolvedValueOnce(YAML.stringify(yarnrc));
|
|
73
|
-
await expect(extractPackageExtensions()).resolves.toBeUndefined();
|
|
74
|
-
expect(readFile).toHaveBeenCalledOnce();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
describe("preparePackageJsonForPnpm", () => {
|
|
78
|
-
beforeEach(() => {
|
|
79
|
-
vi.clearAllMocks();
|
|
80
|
-
});
|
|
81
|
-
test("removes packageManager field and keeps workspaces", async () => {
|
|
82
|
-
const packageJson = {
|
|
83
|
-
name: "test-monorepo",
|
|
84
|
-
packageManager: "yarn@1.22.10",
|
|
85
|
-
version: "1.0.0",
|
|
86
|
-
workspaces: ["packages/*"],
|
|
87
|
-
};
|
|
88
|
-
readFile.mockResolvedValueOnce(JSON.stringify(packageJson, null, 2));
|
|
89
|
-
const workspaces = await preparePackageJsonForPnpm();
|
|
90
|
-
expect(workspaces).toEqual(["packages/*"]);
|
|
91
|
-
expect(readFile).toHaveBeenCalledOnce();
|
|
92
|
-
expect(writeFile).toHaveBeenCalledWith("package.json", JSON.stringify({
|
|
93
|
-
name: packageJson.name,
|
|
94
|
-
version: packageJson.version,
|
|
95
|
-
}, null, 2));
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
describe("writePnpmWorkspaceFile", () => {
|
|
99
|
-
beforeEach(() => {
|
|
100
|
-
vi.clearAllMocks();
|
|
101
|
-
});
|
|
102
|
-
test("writes pnpm-workspace.yaml with given workspaces", async () => {
|
|
103
|
-
const workspaces = ["packages/*", "apps/*"];
|
|
104
|
-
const packageExtensions = {
|
|
105
|
-
"express@*": {
|
|
106
|
-
peerDependencies: {
|
|
107
|
-
typescript: "*",
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
await writePnpmWorkspaceFile(workspaces, packageExtensions);
|
|
112
|
-
expect(writeFile).toHaveBeenCalledWith("pnpm-workspace.yaml", expect.stringContaining(YAML.stringify({ packages: workspaces })), "utf-8");
|
|
113
|
-
expect(writeFile).toHaveBeenCalledWith("pnpm-workspace.yaml", expect.stringContaining(YAML.stringify({ packageExtensions })), "utf-8");
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe("usePnpm", () => {
|
|
117
|
-
beforeEach(() => {
|
|
118
|
-
vi.resetAllMocks();
|
|
119
|
-
});
|
|
120
|
-
test("rejects if the project is already using pnpm", async () => {
|
|
121
|
-
await expect(usePnpm("pnpm", "10.20")).rejects.toThrow();
|
|
122
|
-
});
|
|
123
|
-
test.each([
|
|
124
|
-
{ expected: false, version: "18.0.0" },
|
|
125
|
-
{ expected: false, version: "20.10.0" },
|
|
126
|
-
{ expected: false, version: "20.19.4" },
|
|
127
|
-
{ expected: true, version: "22.0.0" },
|
|
128
|
-
{ expected: true, version: "20.19.5" },
|
|
129
|
-
])("rejects if Node.js version is less than required ($version)", async ({ expected, version }) => {
|
|
130
|
-
stdout.mockResolvedValue("[]");
|
|
131
|
-
readFile.mockResolvedValueOnce(JSON.stringify({ name: "test-monorepo", workspaces: [] }));
|
|
132
|
-
await expect(usePnpm("npm", version).then(() => true, () => false)).resolves.toBe(expected);
|
|
133
|
-
});
|
|
134
|
-
test("moves workspaces from package.json to pnpm-workspace.yaml", async () => {
|
|
135
|
-
const packageJson = {
|
|
136
|
-
name: "test-monorepo",
|
|
137
|
-
packageManager: "yarn@1.22.10",
|
|
138
|
-
version: "1.0.0",
|
|
139
|
-
workspaces: ["packages/*"],
|
|
140
|
-
};
|
|
141
|
-
readFile.mockResolvedValueOnce(JSON.stringify(packageJson, null, 2));
|
|
142
|
-
stdout.mockResolvedValue(`[]`); // npm list workspaces
|
|
143
|
-
await usePnpm("npm", "22.0.0");
|
|
144
|
-
expect(writeFile).toHaveBeenNthCalledWith(2, "pnpm-workspace.yaml", expect.stringContaining(YAML.stringify({
|
|
145
|
-
packages: packageJson.workspaces,
|
|
146
|
-
})), "utf-8");
|
|
147
|
-
});
|
|
148
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import fc from "fast-check";
|
|
2
|
-
/**
|
|
3
|
-
* Tests for the CLI environment schema.
|
|
4
|
-
* Validates that `cliEnvSchema` correctly parses `process.env`.
|
|
5
|
-
*/
|
|
6
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
import { cliEnvSchema } from "../env.js";
|
|
9
|
-
// The accepted stringbool values mirror those recognised by z.stringbool().
|
|
10
|
-
const CI_TRUTHY = ["true", "1", "yes", "y", "on"];
|
|
11
|
-
const CI_FALSEY = ["false", "0", "no", "n", "off"];
|
|
12
|
-
const CI_ACCEPTED = [...CI_TRUTHY, ...CI_FALSEY];
|
|
13
|
-
describe("cliEnvSchema", () => {
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
vi.unstubAllEnvs();
|
|
16
|
-
});
|
|
17
|
-
describe("CI field", () => {
|
|
18
|
-
it("should be true for truthy string-bool values", () => {
|
|
19
|
-
fc.assert(fc.property(fc.constantFrom(...CI_TRUTHY), (value) => {
|
|
20
|
-
vi.stubEnv("CI", value);
|
|
21
|
-
const result = cliEnvSchema.safeParse(process.env);
|
|
22
|
-
expect(result.success).toBe(true);
|
|
23
|
-
expect(result.success && result.data.CI).toBe(true);
|
|
24
|
-
}));
|
|
25
|
-
});
|
|
26
|
-
it("should be false for falsey string-bool values or when unset", () => {
|
|
27
|
-
fc.assert(fc.property(fc.constantFrom(...CI_FALSEY, undefined), (value) => {
|
|
28
|
-
vi.stubEnv("CI", value);
|
|
29
|
-
const result = cliEnvSchema.safeParse(process.env);
|
|
30
|
-
expect(result.success).toBe(true);
|
|
31
|
-
expect(result.success && result.data.CI).toBe(false);
|
|
32
|
-
}));
|
|
33
|
-
});
|
|
34
|
-
it("should not parse unrecognised values", () => {
|
|
35
|
-
fc.assert(fc.property(
|
|
36
|
-
// Every string not in the accepted list
|
|
37
|
-
fc.string().filter((s) => !CI_ACCEPTED.includes(s.toLowerCase())), (value) => {
|
|
38
|
-
vi.stubEnv("CI", value);
|
|
39
|
-
const result = cliEnvSchema.safeParse(process.env);
|
|
40
|
-
expect(result.success).toBe(false);
|
|
41
|
-
expect(result.error).toBeInstanceOf(z.ZodError);
|
|
42
|
-
}));
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { ExecaError } from "execa";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { formatErrorDetailed, toErrorMessage } from "../error-reporting.js";
|
|
4
|
-
describe("toErrorMessage", () => {
|
|
5
|
-
it("returns the string as-is when given a string", () => {
|
|
6
|
-
expect(toErrorMessage("oops")).toBe("oops");
|
|
7
|
-
});
|
|
8
|
-
it("returns 'Unknown error' for null/undefined", () => {
|
|
9
|
-
expect(toErrorMessage(null)).toBe("Unknown error");
|
|
10
|
-
expect(toErrorMessage(undefined)).toBe("Unknown error");
|
|
11
|
-
});
|
|
12
|
-
it("returns Error.message when given a plain Error", () => {
|
|
13
|
-
expect(toErrorMessage(new Error("boom"))).toBe("boom");
|
|
14
|
-
});
|
|
15
|
-
it("prefers ExecaError.shortMessage over message", () => {
|
|
16
|
-
const execaError = Object.assign(Object.create(ExecaError.prototype), {
|
|
17
|
-
message: "long noisy message with stderr",
|
|
18
|
-
shortMessage: "Command failed: terraform init",
|
|
19
|
-
});
|
|
20
|
-
expect(toErrorMessage(execaError)).toBe("Command failed: terraform init");
|
|
21
|
-
});
|
|
22
|
-
it("flattens AggregateError into a bulleted message", () => {
|
|
23
|
-
const aggregate = new AggregateError([new Error("first"), new Error("second")], "parent");
|
|
24
|
-
expect(toErrorMessage(aggregate)).toBe("parent\n - first\n - second");
|
|
25
|
-
});
|
|
26
|
-
it("extracts `message` property from plain objects when present", () => {
|
|
27
|
-
expect(toErrorMessage({ message: "from object" })).toBe("from object");
|
|
28
|
-
});
|
|
29
|
-
it("falls back to JSON.stringify for objects without message", () => {
|
|
30
|
-
expect(toErrorMessage({ code: 42 })).toBe('{"code":42}');
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
describe("formatErrorDetailed", () => {
|
|
34
|
-
it("renders name, message and stack for a single error", () => {
|
|
35
|
-
const err = new Error("top");
|
|
36
|
-
const formatted = formatErrorDetailed(err);
|
|
37
|
-
expect(formatted).toContain("Error: top");
|
|
38
|
-
expect(formatted).toContain("at ");
|
|
39
|
-
});
|
|
40
|
-
it("walks the cause chain", () => {
|
|
41
|
-
const root = new Error("root failure");
|
|
42
|
-
const middle = new Error("middle", { cause: root });
|
|
43
|
-
const top = new Error("top", { cause: middle });
|
|
44
|
-
const formatted = formatErrorDetailed(top);
|
|
45
|
-
expect(formatted).toContain("Error: top");
|
|
46
|
-
expect(formatted).toContain("Caused by: Error: middle");
|
|
47
|
-
expect(formatted).toContain("Caused by: Error: root failure");
|
|
48
|
-
});
|
|
49
|
-
it("terminates when encountering a cycle in the cause chain", () => {
|
|
50
|
-
const a = new Error("a");
|
|
51
|
-
const b = new Error("b", { cause: a });
|
|
52
|
-
a.cause = b;
|
|
53
|
-
const formatted = formatErrorDetailed(a);
|
|
54
|
-
expect(formatted).toContain("Error: a");
|
|
55
|
-
expect(formatted).toContain("Caused by: Error: b");
|
|
56
|
-
});
|
|
57
|
-
it("handles non-Error causes gracefully", () => {
|
|
58
|
-
const err = new Error("wrapped", { cause: "raw string cause" });
|
|
59
|
-
const formatted = formatErrorDetailed(err);
|
|
60
|
-
expect(formatted).toContain("Error: wrapped");
|
|
61
|
-
expect(formatted).toContain("Caused by: raw string cause");
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { exitWithError, isVerbose } from "../index.js";
|
|
4
|
-
/**
|
|
5
|
-
* Builds a parent command that exposes the global `--verbose` flag so that
|
|
6
|
-
* `optsWithGlobals()` behaves the same way it does on the real CLI.
|
|
7
|
-
*/
|
|
8
|
-
const makeProgramWith = (child, argv) => {
|
|
9
|
-
const program = new Command()
|
|
10
|
-
.name("dx")
|
|
11
|
-
.option("-v, --verbose", "verbose output", false)
|
|
12
|
-
.exitOverride()
|
|
13
|
-
.configureOutput({
|
|
14
|
-
writeErr: () => {
|
|
15
|
-
/* silence stderr in tests */
|
|
16
|
-
},
|
|
17
|
-
writeOut: () => {
|
|
18
|
-
/* silence stdout in tests */
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
child.exitOverride().configureOutput({
|
|
22
|
-
writeErr: () => {
|
|
23
|
-
/* silence */
|
|
24
|
-
},
|
|
25
|
-
writeOut: () => {
|
|
26
|
-
/* silence */
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
program.addCommand(child);
|
|
30
|
-
program.parse(argv, { from: "user" });
|
|
31
|
-
return program;
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* `exitWithError` always throws (Commander's `exitOverride()` converts the
|
|
35
|
-
* process.exit call into a CommanderError throw). This helper captures that
|
|
36
|
-
* throw so tests can assert on the thrown payload without putting `expect`
|
|
37
|
-
* inside a `catch` block (which vitest/no-conditional-expect disallows).
|
|
38
|
-
*/
|
|
39
|
-
const captureThrown = (fn) => {
|
|
40
|
-
try {
|
|
41
|
-
fn();
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
return error;
|
|
45
|
-
}
|
|
46
|
-
throw new Error("expected the callback to throw");
|
|
47
|
-
};
|
|
48
|
-
describe("isVerbose", () => {
|
|
49
|
-
it("is false when --verbose is not provided", () => {
|
|
50
|
-
const cmd = new Command("run").action(() => undefined);
|
|
51
|
-
makeProgramWith(cmd, ["run"]);
|
|
52
|
-
expect(isVerbose(cmd)).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
it("is true when -v is provided at the root", () => {
|
|
55
|
-
const cmd = new Command("run").action(() => undefined);
|
|
56
|
-
makeProgramWith(cmd, ["-v", "run"]);
|
|
57
|
-
expect(isVerbose(cmd)).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
it("is true when --verbose is provided at the root", () => {
|
|
60
|
-
const cmd = new Command("run").action(() => undefined);
|
|
61
|
-
makeProgramWith(cmd, ["--verbose", "run"]);
|
|
62
|
-
expect(isVerbose(cmd)).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
describe("exitWithError", () => {
|
|
66
|
-
it("reports only the message in normal mode", () => {
|
|
67
|
-
const cmd = new Command("run").action(() => undefined);
|
|
68
|
-
makeProgramWith(cmd, ["run"]);
|
|
69
|
-
const err = new Error("outer", { cause: new Error("inner secret") });
|
|
70
|
-
expect(() => exitWithError(cmd)(err)).toThrow(/outer/);
|
|
71
|
-
const thrown = captureThrown(() => exitWithError(cmd)(err));
|
|
72
|
-
const message = String(thrown?.message ?? thrown);
|
|
73
|
-
expect(message).not.toContain("Caused by");
|
|
74
|
-
expect(message).not.toContain("inner secret");
|
|
75
|
-
});
|
|
76
|
-
it("includes the cause chain and stack trace in verbose mode", () => {
|
|
77
|
-
const cmd = new Command("run").action(() => undefined);
|
|
78
|
-
makeProgramWith(cmd, ["--verbose", "run"]);
|
|
79
|
-
const root = new Error("root cause");
|
|
80
|
-
const err = new Error("surface", { cause: root });
|
|
81
|
-
const thrown = captureThrown(() => exitWithError(cmd)(err));
|
|
82
|
-
const message = String(thrown?.message ?? thrown);
|
|
83
|
-
expect(message).toContain("Error: surface");
|
|
84
|
-
expect(message).toContain("Caused by: Error: root cause");
|
|
85
|
-
expect(message).toContain("at ");
|
|
86
|
-
});
|
|
87
|
-
it("works with non-Error values", () => {
|
|
88
|
-
const cmd = new Command("run").action(() => undefined);
|
|
89
|
-
makeProgramWith(cmd, ["run"]);
|
|
90
|
-
expect(() => exitWithError(cmd)("plain string failure")).toThrow(/plain string failure/);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { mock } from "vitest-mock-extended";
|
|
3
|
-
import { makeCli } from "../index.js";
|
|
4
|
-
const makeProgram = (argv) => {
|
|
5
|
-
const program = makeCli(mock(), mock(), mock(), "0.0.0");
|
|
6
|
-
program.exitOverride().configureOutput({
|
|
7
|
-
writeErr: () => {
|
|
8
|
-
/* silence stderr in tests */
|
|
9
|
-
},
|
|
10
|
-
writeOut: () => {
|
|
11
|
-
/* silence stdout in tests */
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
program.parseOptions(argv);
|
|
15
|
-
return program;
|
|
16
|
-
};
|
|
17
|
-
describe("--output option", () => {
|
|
18
|
-
it("defaults to 'text' when not provided", () => {
|
|
19
|
-
const program = makeProgram([]);
|
|
20
|
-
expect(program.opts().output).toBe("text");
|
|
21
|
-
});
|
|
22
|
-
it("accepts 'text' explicitly", () => {
|
|
23
|
-
const program = makeProgram(["--output", "text"]);
|
|
24
|
-
expect(program.opts().output).toBe("text");
|
|
25
|
-
});
|
|
26
|
-
it("accepts 'json'", () => {
|
|
27
|
-
const program = makeProgram(["--output", "json"]);
|
|
28
|
-
expect(program.opts().output).toBe("json");
|
|
29
|
-
});
|
|
30
|
-
it("rejects invalid values", () => {
|
|
31
|
-
expect(() => makeProgram(["--output", "xml"])).toThrow();
|
|
32
|
-
});
|
|
33
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for authorizeCloudAccounts in the init command.
|
|
3
|
-
*/
|
|
4
|
-
import { errAsync, okAsync } from "neverthrow";
|
|
5
|
-
import { describe, expect, it } from "vitest";
|
|
6
|
-
import { mock } from "vitest-mock-extended";
|
|
7
|
-
import { AuthorizationError, AuthorizationResult, } from "../../../../domain/authorization.js";
|
|
8
|
-
import { authorizeCloudAccounts } from "../add.js";
|
|
9
|
-
const makeEnvPayload = (overrides = {}) => ({
|
|
10
|
-
env: {
|
|
11
|
-
cloudAccounts: [
|
|
12
|
-
{
|
|
13
|
-
csp: "azure",
|
|
14
|
-
defaultLocation: "italynorth",
|
|
15
|
-
displayName: "DEV-FooBar",
|
|
16
|
-
id: "sub-123",
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
name: "dev",
|
|
20
|
-
prefix: "dx",
|
|
21
|
-
},
|
|
22
|
-
github: { owner: "pagopa", repo: "test-repo" },
|
|
23
|
-
tags: { BusinessUnit: "BU", CostCenter: "TS000", ManagementTeam: "MT" },
|
|
24
|
-
workspace: { domain: "" },
|
|
25
|
-
...overrides,
|
|
26
|
-
});
|
|
27
|
-
describe("authorizeCloudAccounts", () => {
|
|
28
|
-
it("returns empty array when init is undefined (env already initialized)", async () => {
|
|
29
|
-
const authService = mock();
|
|
30
|
-
const envPayload = makeEnvPayload({ init: undefined });
|
|
31
|
-
const result = await authorizeCloudAccounts(authService)(envPayload);
|
|
32
|
-
expect(result.isOk()).toBe(true);
|
|
33
|
-
expect(result._unsafeUnwrap()).toEqual([]);
|
|
34
|
-
expect(authService.requestAuthorization).not.toHaveBeenCalled();
|
|
35
|
-
});
|
|
36
|
-
it("returns empty array when cloudAccountsToInitialize is empty", async () => {
|
|
37
|
-
const authService = mock();
|
|
38
|
-
const envPayload = makeEnvPayload({
|
|
39
|
-
init: { cloudAccountsToInitialize: [] },
|
|
40
|
-
});
|
|
41
|
-
const result = await authorizeCloudAccounts(authService)(envPayload);
|
|
42
|
-
expect(result.isOk()).toBe(true);
|
|
43
|
-
expect(result._unsafeUnwrap()).toEqual([]);
|
|
44
|
-
});
|
|
45
|
-
it("requests authorization for each initialized account", async () => {
|
|
46
|
-
const authService = mock();
|
|
47
|
-
const expectedPr = new AuthorizationResult("https://github.com/pagopa/eng-azure-authorization/pull/42");
|
|
48
|
-
authService.requestAuthorization.mockReturnValue(okAsync(expectedPr));
|
|
49
|
-
const account = {
|
|
50
|
-
csp: "azure",
|
|
51
|
-
defaultLocation: "italynorth",
|
|
52
|
-
displayName: "DEV-FooBar",
|
|
53
|
-
id: "sub-123",
|
|
54
|
-
};
|
|
55
|
-
const envPayload = makeEnvPayload({
|
|
56
|
-
env: {
|
|
57
|
-
cloudAccounts: [account],
|
|
58
|
-
name: "dev",
|
|
59
|
-
prefix: "dx",
|
|
60
|
-
},
|
|
61
|
-
init: { cloudAccountsToInitialize: [account] },
|
|
62
|
-
});
|
|
63
|
-
const result = await authorizeCloudAccounts(authService)(envPayload);
|
|
64
|
-
expect(result.isOk()).toBe(true);
|
|
65
|
-
expect(result._unsafeUnwrap()).toEqual([expectedPr]);
|
|
66
|
-
expect(authService.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
|
|
67
|
-
bootstrapIdentityId: "dx-d-itn-bootstrap-id-01",
|
|
68
|
-
envShort: "d",
|
|
69
|
-
prefix: "dx",
|
|
70
|
-
subscriptionName: "DEV-FooBar",
|
|
71
|
-
}));
|
|
72
|
-
});
|
|
73
|
-
it("computes correct identity for westeurope location", async () => {
|
|
74
|
-
const authService = mock();
|
|
75
|
-
authService.requestAuthorization.mockReturnValue(okAsync(new AuthorizationResult("https://example.com/pr/1")));
|
|
76
|
-
const account = {
|
|
77
|
-
csp: "azure",
|
|
78
|
-
defaultLocation: "westeurope",
|
|
79
|
-
displayName: "PROD-Bar",
|
|
80
|
-
id: "sub-456",
|
|
81
|
-
};
|
|
82
|
-
const envPayload = makeEnvPayload({
|
|
83
|
-
env: {
|
|
84
|
-
cloudAccounts: [account],
|
|
85
|
-
name: "prod",
|
|
86
|
-
prefix: "io",
|
|
87
|
-
},
|
|
88
|
-
init: { cloudAccountsToInitialize: [account] },
|
|
89
|
-
});
|
|
90
|
-
const result = await authorizeCloudAccounts(authService)(envPayload);
|
|
91
|
-
expect(result.isOk()).toBe(true);
|
|
92
|
-
expect(authService.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
|
|
93
|
-
bootstrapIdentityId: "io-p-weu-bootstrap-id-01",
|
|
94
|
-
envShort: "p",
|
|
95
|
-
prefix: "io",
|
|
96
|
-
subscriptionName: "PROD-Bar",
|
|
97
|
-
}));
|
|
98
|
-
});
|
|
99
|
-
it("skips accounts with unsupported locations", async () => {
|
|
100
|
-
const authService = mock();
|
|
101
|
-
const account = {
|
|
102
|
-
csp: "azure",
|
|
103
|
-
defaultLocation: "eastus",
|
|
104
|
-
displayName: "DEV-Unsupported",
|
|
105
|
-
id: "sub-789",
|
|
106
|
-
};
|
|
107
|
-
const envPayload = makeEnvPayload({
|
|
108
|
-
init: { cloudAccountsToInitialize: [account] },
|
|
109
|
-
});
|
|
110
|
-
const result = await authorizeCloudAccounts(authService)(envPayload);
|
|
111
|
-
expect(result.isOk()).toBe(true);
|
|
112
|
-
expect(result._unsafeUnwrap()).toEqual([]);
|
|
113
|
-
expect(authService.requestAuthorization).not.toHaveBeenCalled();
|
|
114
|
-
});
|
|
115
|
-
it("continues when authorization fails for one account", async () => {
|
|
116
|
-
const authService = mock();
|
|
117
|
-
const account1 = {
|
|
118
|
-
csp: "azure",
|
|
119
|
-
defaultLocation: "italynorth",
|
|
120
|
-
displayName: "DEV-First",
|
|
121
|
-
id: "sub-1",
|
|
122
|
-
};
|
|
123
|
-
const account2 = {
|
|
124
|
-
csp: "azure",
|
|
125
|
-
defaultLocation: "italynorth",
|
|
126
|
-
displayName: "DEV-Second",
|
|
127
|
-
id: "sub-2",
|
|
128
|
-
};
|
|
129
|
-
const expectedPr = new AuthorizationResult("https://example.com/pr/2");
|
|
130
|
-
authService.requestAuthorization
|
|
131
|
-
.mockReturnValueOnce(errAsync(new AuthorizationError("Branch already exists")))
|
|
132
|
-
.mockReturnValueOnce(okAsync(expectedPr));
|
|
133
|
-
const envPayload = makeEnvPayload({
|
|
134
|
-
env: {
|
|
135
|
-
cloudAccounts: [account1, account2],
|
|
136
|
-
name: "dev",
|
|
137
|
-
prefix: "dx",
|
|
138
|
-
},
|
|
139
|
-
init: { cloudAccountsToInitialize: [account1, account2] },
|
|
140
|
-
});
|
|
141
|
-
const result = await authorizeCloudAccounts(authService)(envPayload);
|
|
142
|
-
expect(result.isOk()).toBe(true);
|
|
143
|
-
const prs = result._unsafeUnwrap();
|
|
144
|
-
expect(prs).toHaveLength(1);
|
|
145
|
-
expect(prs[0]).toEqual(expectedPr);
|
|
146
|
-
});
|
|
147
|
-
it("returns a no-op authorization result when nothing changed", async () => {
|
|
148
|
-
const authService = mock();
|
|
149
|
-
const account = {
|
|
150
|
-
csp: "azure",
|
|
151
|
-
defaultLocation: "italynorth",
|
|
152
|
-
displayName: "DEV-Existing",
|
|
153
|
-
id: "sub-exists",
|
|
154
|
-
};
|
|
155
|
-
// No-op result: Ok with no URL (identity + groups already configured)
|
|
156
|
-
authService.requestAuthorization.mockReturnValue(okAsync(new AuthorizationResult()));
|
|
157
|
-
const envPayload = makeEnvPayload({
|
|
158
|
-
init: { cloudAccountsToInitialize: [account] },
|
|
159
|
-
});
|
|
160
|
-
const result = await authorizeCloudAccounts(authService)(envPayload);
|
|
161
|
-
expect(result.isOk()).toBe(true);
|
|
162
|
-
// The no-op result is still collected but has no URL
|
|
163
|
-
const prs = result._unsafeUnwrap();
|
|
164
|
-
expect(prs).toHaveLength(1);
|
|
165
|
-
expect(prs[0].url).toBeUndefined();
|
|
166
|
-
});
|
|
167
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for confirmGitHubRepoCreation in the init command.
|
|
3
|
-
*/
|
|
4
|
-
import inquirer from "inquirer";
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import { confirmGitHubRepoCreation } from "../init.js";
|
|
7
|
-
vi.mock("inquirer");
|
|
8
|
-
const makePayload = (overrides = {}) => ({
|
|
9
|
-
repoDescription: "A test repo",
|
|
10
|
-
repoName: "test-repo",
|
|
11
|
-
repoOwner: "pagopa",
|
|
12
|
-
...overrides,
|
|
13
|
-
});
|
|
14
|
-
describe("confirmGitHubRepoCreation", () => {
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
vi.restoreAllMocks();
|
|
17
|
-
});
|
|
18
|
-
it("returns true when the user confirms", async () => {
|
|
19
|
-
vi.mocked(inquirer.prompt).mockResolvedValue({ confirm: true });
|
|
20
|
-
const result = await confirmGitHubRepoCreation(makePayload());
|
|
21
|
-
expect(result.isOk()).toBe(true);
|
|
22
|
-
expect(result._unsafeUnwrap()).toBe(true);
|
|
23
|
-
});
|
|
24
|
-
it("returns false when the user declines", async () => {
|
|
25
|
-
vi.mocked(inquirer.prompt).mockResolvedValue({ confirm: false });
|
|
26
|
-
const result = await confirmGitHubRepoCreation(makePayload());
|
|
27
|
-
expect(result.isOk()).toBe(true);
|
|
28
|
-
expect(result._unsafeUnwrap()).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
it("prompts with the correct repository name and owner", async () => {
|
|
31
|
-
vi.mocked(inquirer.prompt).mockResolvedValue({ confirm: true });
|
|
32
|
-
const payload = makePayload({ repoName: "my-repo", repoOwner: "my-org" });
|
|
33
|
-
await confirmGitHubRepoCreation(payload);
|
|
34
|
-
expect(inquirer.prompt).toHaveBeenCalledWith(expect.objectContaining({
|
|
35
|
-
message: expect.stringContaining("my-org/my-repo"),
|
|
36
|
-
type: "confirm",
|
|
37
|
-
}));
|
|
38
|
-
});
|
|
39
|
-
it("returns an error result when the prompt rejects", async () => {
|
|
40
|
-
const cause = new Error("non-interactive TTY");
|
|
41
|
-
vi.mocked(inquirer.prompt).mockRejectedValue(cause);
|
|
42
|
-
const result = await confirmGitHubRepoCreation(makePayload());
|
|
43
|
-
expect(result.isErr()).toBe(true);
|
|
44
|
-
const err = result._unsafeUnwrapErr();
|
|
45
|
-
expect(err).toBeInstanceOf(Error);
|
|
46
|
-
expect(err.cause).toBe(cause);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
const mocks = vi.hoisted(() => ({
|
|
3
|
-
oraPromise: vi.fn((promise) => promise),
|
|
4
|
-
tf$: vi.fn(async (...args) => {
|
|
5
|
-
void args;
|
|
6
|
-
return { stdout: '{"user":{"name":"test@example.com"}}' };
|
|
7
|
-
}),
|
|
8
|
-
}));
|
|
9
|
-
vi.mock("ora", () => ({ oraPromise: mocks.oraPromise }));
|
|
10
|
-
vi.mock("../../../execa/terraform.js", () => ({ tf$: mocks.tf$ }));
|
|
11
|
-
import { checkAddEnvironmentPreconditions, checkInitPreconditions, } from "../init.js";
|
|
12
|
-
const calledCommands = () => mocks.tf$.mock.calls.map(([strings, ...values]) => strings.reduce((command, part, index) => command + part + String(values[index] ?? ""), ""));
|
|
13
|
-
describe("init preconditions", () => {
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
vi.clearAllMocks();
|
|
16
|
-
});
|
|
17
|
-
it("checkInitPreconditions does not require Azure login", async () => {
|
|
18
|
-
const result = await checkInitPreconditions();
|
|
19
|
-
expect(result.isOk()).toBe(true);
|
|
20
|
-
expect(calledCommands()).toEqual(["terraform -version", "corepack -v"]);
|
|
21
|
-
});
|
|
22
|
-
it("checkAddEnvironmentPreconditions requires Azure login", async () => {
|
|
23
|
-
const result = await checkAddEnvironmentPreconditions();
|
|
24
|
-
expect(result.isOk()).toBe(true);
|
|
25
|
-
expect(calledCommands()).toEqual([
|
|
26
|
-
"terraform -version",
|
|
27
|
-
"az account show",
|
|
28
|
-
"az group list",
|
|
29
|
-
"corepack -v",
|
|
30
|
-
]);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|