Skip to content

Commit 4f893d1

Browse files
committed
feat(publish): Eager prompt for OTP when account-level 2FA is enabled
1 parent 0501f7a commit 4f893d1

15 files changed

Lines changed: 239 additions & 0 deletions
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"use strict";
2+
3+
jest.mock("../lib/get-profile-data");
4+
5+
const loggingOutput = require("@lerna-test/logging-output");
6+
const getProfileData = require("../lib/get-profile-data");
7+
const getTwoFactorAuthRequired = require("../lib/get-two-factor-auth-required");
8+
9+
getProfileData.mockImplementation(() => Promise.resolve({ tfa: {} }));
10+
11+
expect.extend(require("@lerna-test/figgy-pudding-matchers"));
12+
13+
describe("getTwoFactorAuthRequired", () => {
14+
const origConsoleError = console.error;
15+
16+
beforeEach(() => {
17+
console.error = jest.fn();
18+
});
19+
20+
afterEach(() => {
21+
console.error = origConsoleError;
22+
});
23+
24+
it("resolves true if tfa.mode === 'auth-and-writes'", async () => {
25+
getProfileData.mockResolvedValueOnce({
26+
tfa: {
27+
mode: "auth-and-writes",
28+
},
29+
});
30+
31+
const result = await getTwoFactorAuthRequired();
32+
expect(result).toBe(true);
33+
expect(getProfileData).toHaveBeenLastCalledWith(expect.figgyPudding({ "fetch-retries": 0 }));
34+
});
35+
36+
it("resolves false if tfa.mode !== 'auth-and-writes'", async () => {
37+
getProfileData.mockResolvedValueOnce({
38+
tfa: {
39+
mode: "auth-only",
40+
},
41+
});
42+
43+
const result = await getTwoFactorAuthRequired();
44+
expect(result).toBe(false);
45+
});
46+
47+
it("resolves false if tfa.pending === true", async () => {
48+
getProfileData.mockResolvedValueOnce({
49+
tfa: {
50+
pending: true,
51+
mode: "ignored",
52+
},
53+
});
54+
55+
const result = await getTwoFactorAuthRequired();
56+
expect(result).toBe(false);
57+
});
58+
59+
it("resolves false after profile 404", async () => {
60+
getProfileData.mockImplementationOnce(() => {
61+
const err = new Error("third-party profile fail");
62+
63+
err.code = "E404";
64+
65+
return Promise.reject(err);
66+
});
67+
68+
const result = await getTwoFactorAuthRequired();
69+
70+
expect(result).toBe(false);
71+
expect(console.error).not.toHaveBeenCalled();
72+
});
73+
74+
it("resolves false after profile 500", async () => {
75+
getProfileData.mockImplementationOnce(() => {
76+
const err = new Error("legacy npm Enterprise profile fail");
77+
78+
err.code = "E500";
79+
80+
return Promise.reject(err);
81+
});
82+
83+
const opts = new Map([["registry", "such-registry-wow"]]);
84+
const result = await getTwoFactorAuthRequired(opts);
85+
86+
expect(result).toBe(false);
87+
expect(loggingOutput("warn")).toContain(
88+
`Registry "${opts.get(
89+
"registry"
90+
)}" does not support 'npm profile get', skipping two-factor auth check...`
91+
);
92+
});
93+
94+
it("logs unexpected failure message before throwing validation error", async () => {
95+
getProfileData.mockImplementationOnce(() => {
96+
const err = new Error("zomg explosions");
97+
98+
err.code = "E401";
99+
100+
return Promise.reject(err);
101+
});
102+
103+
try {
104+
await getTwoFactorAuthRequired();
105+
} catch (err) {
106+
expect(err.prefix).toBe("ETWOFACTOR");
107+
expect(err.message).toBe("Unable to obtain two-factor auth mode");
108+
expect(console.error).toHaveBeenCalledWith("zomg explosions");
109+
}
110+
111+
expect.assertions(3);
112+
});
113+
});

commands/publish/__tests__/publish-canary-no-git-reset.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ jest.unmock("@lerna/collect-updates");
77
jest.mock("../lib/get-packages-without-license");
88
jest.mock("../lib/verify-npm-package-access");
99
jest.mock("../lib/get-npm-username");
10+
jest.mock("../lib/get-two-factor-auth-required");
1011
jest.mock("../lib/git-checkout");
1112

1213
const fs = require("fs-extra");

commands/publish/__tests__/publish-canary.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ jest.unmock("@lerna/collect-updates");
77
jest.mock("../lib/get-packages-without-license");
88
jest.mock("../lib/verify-npm-package-access");
99
jest.mock("../lib/get-npm-username");
10+
jest.mock("../lib/get-two-factor-auth-required");
1011

1112
const fs = require("fs-extra");
1213
const path = require("path");

commands/publish/__tests__/publish-command.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"use strict";
22

3+
jest.mock("@lerna/otplease");
4+
35
// local modules _must_ be explicitly mocked
46
jest.mock("../lib/get-packages-without-license");
57
jest.mock("../lib/verify-npm-package-access");
68
jest.mock("../lib/get-npm-username");
9+
jest.mock("../lib/get-two-factor-auth-required");
710
jest.mock("../lib/get-unpublished-packages");
811
// FIXME: better mock for version command
912
jest.mock("../../version/lib/git-push");
@@ -12,13 +15,15 @@ jest.mock("../../version/lib/is-behind-upstream");
1215
jest.mock("../../version/lib/remote-branch-exists");
1316

1417
// mocked or stubbed modules
18+
const otplease = require("@lerna/otplease");
1519
const npmDistTag = require("@lerna/npm-dist-tag");
1620
const npmPublish = require("@lerna/npm-publish");
1721
const packDirectory = require("@lerna/pack-directory");
1822
const PromptUtilities = require("@lerna/prompt");
1923
const collectUpdates = require("@lerna/collect-updates");
2024
const getNpmUsername = require("../lib/get-npm-username");
2125
const verifyNpmPackageAccess = require("../lib/verify-npm-package-access");
26+
const getTwoFactorAuthRequired = require("../lib/get-two-factor-auth-required");
2227

2328
// helpers
2429
const commitChangeToPackage = require("@lerna-test/commit-change-to-package");
@@ -127,6 +132,12 @@ Map {
127132
"lerna-test",
128133
expect.figgyPudding({ registry: "https://registry.npmjs.org/" })
129134
);
135+
136+
expect(getTwoFactorAuthRequired).toHaveBeenCalled();
137+
expect(getTwoFactorAuthRequired).toHaveBeenLastCalledWith(
138+
// extra insurance that @lerna/npm-conf is defaulting things correctly
139+
expect.figgyPudding({ otp: undefined })
140+
);
130141
});
131142

132143
it("publishes changed independent packages", async () => {
@@ -168,10 +179,15 @@ Map {
168179
});
169180

170181
describe("--otp", () => {
182+
otplease.getOneTimePassword.mockImplementation(() => Promise.resolve("654321"));
183+
171184
it("passes one-time password to npm commands", async () => {
172185
const testDir = await initFixture("normal");
173186
const otp = "123456";
174187

188+
// cli option skips prompt
189+
getTwoFactorAuthRequired.mockResolvedValueOnce(true);
190+
175191
await lernaPublish(testDir)("--otp", otp);
176192

177193
expect(npmPublish).toHaveBeenCalledWith(
@@ -180,6 +196,23 @@ Map {
180196
expect.objectContaining({ otp }),
181197
expect.objectContaining({ otp })
182198
);
199+
expect(otplease.getOneTimePassword).not.toHaveBeenCalled();
200+
});
201+
202+
it("prompts for OTP when option missing and account-level 2FA enabled", async () => {
203+
const testDir = await initFixture("normal");
204+
205+
getTwoFactorAuthRequired.mockResolvedValueOnce(true);
206+
207+
await lernaPublish(testDir)();
208+
209+
expect(npmPublish).toHaveBeenCalledWith(
210+
expect.objectContaining({ name: "package-1" }),
211+
"/TEMP_DIR/package-1-MOCKED.tgz",
212+
expect.objectContaining({ otp: undefined }),
213+
expect.objectContaining({ otp: "654321" })
214+
);
215+
expect(otplease.getOneTimePassword).toHaveBeenLastCalledWith("Enter OTP:");
183216
});
184217
});
185218

commands/publish/__tests__/publish-from-git.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
jest.mock("../lib/get-packages-without-license");
55
jest.mock("../lib/verify-npm-package-access");
66
jest.mock("../lib/get-npm-username");
7+
jest.mock("../lib/get-two-factor-auth-required");
78
jest.mock("../lib/get-unpublished-packages");
89
// FIXME: better mock for version command
910
jest.mock("../../version/lib/git-push");

commands/publish/__tests__/publish-from-package.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
jest.mock("../lib/get-packages-without-license");
55
jest.mock("../lib/verify-npm-package-access");
66
jest.mock("../lib/get-npm-username");
7+
jest.mock("../lib/get-two-factor-auth-required");
78
jest.mock("../lib/get-unpublished-packages");
89
// FIXME: better mock for version command
910
jest.mock("../../version/lib/git-push");

commands/publish/__tests__/publish-licenses.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// local modules _must_ be explicitly mocked
44
jest.mock("../lib/verify-npm-package-access");
55
jest.mock("../lib/get-npm-username");
6+
jest.mock("../lib/get-two-factor-auth-required");
67
jest.mock("../lib/create-temp-licenses", () => jest.fn(() => Promise.resolve()));
78
jest.mock("../lib/remove-temp-licenses", () => jest.fn(() => Promise.resolve()));
89
// FIXME: better mock for version command

commands/publish/__tests__/publish-lifecycle-scripts.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
jest.mock("../lib/get-packages-without-license");
55
jest.mock("../lib/verify-npm-package-access");
66
jest.mock("../lib/get-npm-username");
7+
jest.mock("../lib/get-two-factor-auth-required");
78
// FIXME: better mock for version command
89
jest.mock("../../version/lib/git-push");
910
jest.mock("../../version/lib/is-anything-committed");

commands/publish/__tests__/publish-relative-file-specifiers.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ jest.unmock("@lerna/collect-updates");
77
jest.mock("../lib/get-packages-without-license");
88
jest.mock("../lib/verify-npm-package-access");
99
jest.mock("../lib/get-npm-username");
10+
jest.mock("../lib/get-two-factor-auth-required");
1011
// FIXME: better mock for version command
1112
jest.mock("../../version/lib/git-push");
1213
jest.mock("../../version/lib/is-anything-committed");

commands/publish/__tests__/publish-tagging.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
jest.mock("../lib/get-packages-without-license");
55
jest.mock("../lib/verify-npm-package-access");
66
jest.mock("../lib/get-npm-username");
7+
jest.mock("../lib/get-two-factor-auth-required");
78
// FIXME: better mock for version command
89
jest.mock("../../version/lib/git-push");
910
jest.mock("../../version/lib/is-anything-committed");

0 commit comments

Comments
 (0)