From e1327123518d1e629d59a6724b8a3f2dcc09a731 Mon Sep 17 00:00:00 2001 From: Xavier Rutayisire Date: Thu, 26 Oct 2023 09:55:29 +0200 Subject: [PATCH] fix(init): Fix failed to create repository error in init (#1179) --- packages/init/src/SliceMachineInitProcess.ts | 77 ++++++++++----- ...ineInitProcess-createNewRepository.test.ts | 97 ++++++++++++++++++- 2 files changed, 145 insertions(+), 29 deletions(-) diff --git a/packages/init/src/SliceMachineInitProcess.ts b/packages/init/src/SliceMachineInitProcess.ts index 2e9ed0b6c3..50006892a3 100644 --- a/packages/init/src/SliceMachineInitProcess.ts +++ b/packages/init/src/SliceMachineInitProcess.ts @@ -267,7 +267,7 @@ Continue with next steps in Slice Machine. if (ctLibrary.ids.length === 0) { return true; } - } catch (e) { + } catch (error) { return true; } @@ -549,25 +549,9 @@ Continue with next steps in Slice Machine. if (!isLoggedIn) { parentTask.output = "Press any key to open the browser to login..."; - await new Promise((resolve) => { - const initialRawMode = !!process.stdin.isRaw; - process.stdin.setRawMode?.(true); - process.stdin.resume(); - process.stdin.once("data", (data: Buffer) => { - process.stdin.setRawMode?.(initialRawMode); - process.stdin.pause(); - resolve(data.toString("utf-8")); - }); - }); - + await this.pressKeyToLogin(); parentTask.output = "Browser opened, waiting for you to login..."; - const { port, url } = await this.manager.user.getLoginSessionInfo(); - await this.manager.user.nodeLoginSession({ - port, - onListenCallback() { - open(url); - }, - }); + await this.waitingForLogin(); } parentTask.output = ""; @@ -600,6 +584,29 @@ Continue with next steps in Slice Machine. ]); } + protected async pressKeyToLogin(): Promise { + await new Promise((resolve) => { + const initialRawMode = !!process.stdin.isRaw; + process.stdin.setRawMode?.(true); + process.stdin.resume(); + process.stdin.once("data", (data: Buffer) => { + process.stdin.setRawMode?.(initialRawMode); + process.stdin.pause(); + resolve(data.toString("utf-8")); + }); + }); + } + + protected async waitingForLogin(): Promise { + const { port, url } = await this.manager.user.getLoginSessionInfo(); + await this.manager.user.nodeLoginSession({ + port, + onListenCallback() { + open(url); + }, + }); + } + protected useRepositoryFlag(): Promise { return listrRun([ { @@ -871,13 +878,35 @@ ${chalk.cyan("?")} Your Prismic repository name`.replace("\n", ""), "Project framework must be available through context to run `createNewRepository`", ); - await this.manager.prismicRepository.create({ - domain: this.context.repository.domain, - framework: this.context.framework.wroomTelemetryID, - starterId: this.context.starterId, - }); + try { + await this.manager.prismicRepository.create({ + domain: this.context.repository.domain, + framework: this.context.framework.wroomTelemetryID, + starterId: this.context.starterId, + }); + } catch (error) { + // When we have an error here, it's most probably because the user has a stale SESSION cookie + + // Ensure to logout user to remove SESSION and prismic-auth cookies + await this.manager.user.logout(); + + // Force a new login to get a brand new cookies value + task.output = + "It seems there is an authentication problem, press any key to open the browser to login again..."; + await this.pressKeyToLogin(); + task.output = "Browser opened, waiting for you to login..."; + await this.waitingForLogin(); + + // Try to create repository again with the new cookies value + await this.manager.prismicRepository.create({ + domain: this.context.repository.domain, + framework: this.context.framework.wroomTelemetryID, + starterId: this.context.starterId, + }); + } this.context.repository.exists = true; + task.output = ""; task.title = `Created new repository ${chalk.cyan( this.context.repository.domain, )}`; diff --git a/packages/init/test/SliceMachineInitProcess-createNewRepository.test.ts b/packages/init/test/SliceMachineInitProcess-createNewRepository.test.ts index 4cc6148d63..d95b656192 100644 --- a/packages/init/test/SliceMachineInitProcess-createNewRepository.test.ts +++ b/packages/init/test/SliceMachineInitProcess-createNewRepository.test.ts @@ -1,9 +1,14 @@ -import { beforeEach, expect, it, TestContext } from "vitest"; +import http from "node:http"; +import { beforeEach, expect, it, TestContext, vi } from "vitest"; +import { stdin as mockStdin } from "mock-stdin"; import { createSliceMachineInitProcess, SliceMachineInitProcess } from "../src"; import { UNIVERSAL } from "../src/lib/framework"; -import { createPrismicAuthLoginResponse } from "./__testutils__/createPrismicAuthLoginResponse"; +import { + createPrismicAuthLoginResponse, + PrismicAuthLoginResponse, +} from "./__testutils__/createPrismicAuthLoginResponse"; import { mockPrismicRepositoryAPI } from "./__testutils__/mockPrismicRepositoryAPI"; import { mockPrismicUserAPI } from "./__testutils__/mockPrismicUserAPI"; import { mockPrismicAuthAPI } from "./__testutils__/mockPrismicAuthAPI"; @@ -16,6 +21,12 @@ import { watchStd } from "./__testutils__/watchStd"; const initProcess = createSliceMachineInitProcess(); const spiedManager = spyManager(initProcess); +vi.mock("open", () => { + return { + default: vi.fn(), + }; +}); + beforeEach(() => { setContext(initProcess, { framework: UNIVERSAL, @@ -30,7 +41,9 @@ const mockPrismicAPIs = async ( ctx: TestContext, initProcess: SliceMachineInitProcess, domain?: string, -): Promise => { +): Promise<{ + prismicAuthLoginResponse: PrismicAuthLoginResponse; +}> => { const prismicAuthLoginResponse = createPrismicAuthLoginResponse(); mockPrismicUserAPI(ctx); mockPrismicAuthAPI(ctx); @@ -44,9 +57,42 @@ const mockPrismicAPIs = async ( domain: domain ?? initProcess.context.repository?.domain, }, }); + + return { prismicAuthLoginResponse }; }; -it.skip("creates repository from context", async (ctx) => { +const loginWithStdin = async ( + prismicAuthLoginResponse: PrismicAuthLoginResponse, +) => { + const stdin = mockStdin(); + + await new Promise((res) => setTimeout(res, 50)); + expect(spiedManager.prismicRepository.create).toHaveBeenCalledOnce(); + + stdin.send("o").restore(); + await new Promise((res) => setTimeout(res, 50)); + + const port: number = + spiedManager.user.getLoginSessionInfo.mock.results[0].value.port; + const body = JSON.stringify(prismicAuthLoginResponse); + + // We use low-level `http` because node-fetch has some issue with 127.0.0.1 on CIs + const request = http.request({ + host: "127.0.0.1", + port: `${port}`, + path: "/", + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(body), + }, + }); + request.write(body); + request.end(); + await new Promise((res) => setTimeout(res, 50)); +}; + +it("create repository from context", async (ctx) => { await mockPrismicAPIs(ctx, initProcess); await watchStd(async () => { @@ -57,12 +103,53 @@ it.skip("creates repository from context", async (ctx) => { expect(spiedManager.prismicRepository.create).toHaveBeenCalledOnce(); expect(spiedManager.prismicRepository.create).toHaveBeenNthCalledWith(1, { domain: "new-repo", - framework: UNIVERSAL.sliceMachineTelemetryID, + framework: UNIVERSAL.wroomTelemetryID, + starterId: undefined, }); // @ts-expect-error - Accessing protected property expect(initProcess.context.repository?.exists).toBe(true); }); +it("creates repository with a retry after a first fail", async (ctx) => { + const { prismicAuthLoginResponse } = await mockPrismicAPIs(ctx, initProcess); + + // Mock only the first create call to reject first one and resole the second one + spiedManager.prismicRepository.create.mockImplementationOnce(() => { + return Promise.reject(new Error("Failed to create repository")); + }); + + await watchStd(async () => { + // @ts-expect-error - Accessing protected method + initProcess.createNewRepository(); + await loginWithStdin(prismicAuthLoginResponse); + }); + + expect(spiedManager.prismicRepository.create).toHaveBeenCalledTimes(2); + // @ts-expect-error - Accessing protected property + expect(initProcess.context.repository?.exists).toBe(true); +}); + +it("fail to create repository after a second fail", async (ctx) => { + const { prismicAuthLoginResponse } = await mockPrismicAPIs(ctx, initProcess); + + // Mock create call to reject first and second calls + spiedManager.prismicRepository.create.mockImplementation(() => { + return Promise.reject(new Error("Failed to create repository")); + }); + + await watchStd(async () => { + // @ts-expect-error - Accessing protected method + expect(initProcess.createNewRepository()).rejects.toThrow( + "Failed to create repository", + ); + await loginWithStdin(prismicAuthLoginResponse); + }); + + expect(spiedManager.prismicRepository.create).toHaveBeenCalledTimes(2); + // @ts-expect-error - Accessing protected property + expect(initProcess.context.repository?.exists).toBe(false); +}); + it("throws if context is missing framework", async () => { updateContext(initProcess, { framework: undefined,