|
| 1 | +import * as FileSystem from "@effect/platform/FileSystem" |
| 2 | +import * as Path from "@effect/platform/Path" |
| 3 | +import { NodeContext } from "@effect/platform-node" |
| 4 | +import { describe, expect, it } from "@effect/vitest" |
| 5 | +import { Effect } from "effect" |
| 6 | +import { vi } from "vitest" |
| 7 | + |
| 8 | +import type { CreateCommand, TemplateConfig } from "../../src/core/domain.js" |
| 9 | +import { createProject } from "../../src/usecases/actions/create-project.js" |
| 10 | +import { |
| 11 | + githubInvalidTokenMessage, |
| 12 | + resolveGithubCloneAuthToken |
| 13 | +} from "../../src/usecases/github-token-preflight.js" |
| 14 | + |
| 15 | +const withTempDir = <A, E, R>( |
| 16 | + use: (tempDir: string) => Effect.Effect<A, E, R> |
| 17 | +): Effect.Effect<A, E, R | FileSystem.FileSystem> => |
| 18 | + Effect.scoped( |
| 19 | + Effect.gen(function*(_) { |
| 20 | + const fs = yield* _(FileSystem.FileSystem) |
| 21 | + const tempDir = yield* _( |
| 22 | + fs.makeTempDirectoryScoped({ |
| 23 | + prefix: "docker-git-github-token-preflight-" |
| 24 | + }) |
| 25 | + ) |
| 26 | + return yield* _(use(tempDir)) |
| 27 | + }) |
| 28 | + ) |
| 29 | + |
| 30 | +const withPatchedFetch = <A, E, R>( |
| 31 | + fetchImpl: typeof globalThis.fetch, |
| 32 | + effect: Effect.Effect<A, E, R> |
| 33 | +): Effect.Effect<A, E, R> => |
| 34 | + Effect.acquireUseRelease( |
| 35 | + Effect.sync(() => { |
| 36 | + const previous = globalThis.fetch |
| 37 | + globalThis.fetch = fetchImpl |
| 38 | + return previous |
| 39 | + }), |
| 40 | + () => effect, |
| 41 | + (previous) => |
| 42 | + Effect.sync(() => { |
| 43 | + globalThis.fetch = previous |
| 44 | + }) |
| 45 | + ) |
| 46 | + |
| 47 | +const makeCommand = (root: string, outDir: string, path: Path.Path): CreateCommand => { |
| 48 | + const template: TemplateConfig = { |
| 49 | + containerName: "dg-test", |
| 50 | + serviceName: "dg-test", |
| 51 | + sshUser: "dev", |
| 52 | + sshPort: 2222, |
| 53 | + repoUrl: "https://github.com/TelegramGPT/go-login-ozon.git", |
| 54 | + repoRef: "main", |
| 55 | + targetDir: "/home/dev/workspaces/telegramgpt/go-login-ozon", |
| 56 | + volumeName: "dg-test-home", |
| 57 | + dockerGitPath: path.join(root, ".docker-git"), |
| 58 | + authorizedKeysPath: path.join(root, "authorized_keys"), |
| 59 | + envGlobalPath: path.join(root, ".orch/env/global.env"), |
| 60 | + envProjectPath: path.join(root, ".orch/env/project.env"), |
| 61 | + codexAuthPath: path.join(root, ".orch/auth/codex"), |
| 62 | + codexSharedAuthPath: path.join(root, ".orch/auth/codex-shared"), |
| 63 | + codexHome: "/home/dev/.codex", |
| 64 | + dockerNetworkMode: "shared", |
| 65 | + dockerSharedNetworkName: "docker-git-shared", |
| 66 | + enableMcpPlaywright: false, |
| 67 | + pnpmVersion: "10.27.0" |
| 68 | + } |
| 69 | + |
| 70 | + return { |
| 71 | + _tag: "Create", |
| 72 | + config: template, |
| 73 | + outDir, |
| 74 | + runUp: false, |
| 75 | + openSsh: false, |
| 76 | + force: true, |
| 77 | + forceEnv: false, |
| 78 | + waitForClone: true |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +describe("github token preflight", () => { |
| 83 | + it("prefers the owner-labeled token over the default token", () => { |
| 84 | + const envText = [ |
| 85 | + "# docker-git env", |
| 86 | + "GITHUB_TOKEN=default-token", |
| 87 | + "GITHUB_TOKEN__TELEGRAMGPT=labeled-token", |
| 88 | + "" |
| 89 | + ].join("\n") |
| 90 | + |
| 91 | + const token = resolveGithubCloneAuthToken(envText, { |
| 92 | + repoUrl: "https://github.com/TelegramGPT/go-login-ozon.git", |
| 93 | + gitTokenLabel: undefined |
| 94 | + }) |
| 95 | + |
| 96 | + expect(token).toBe("labeled-token") |
| 97 | + }) |
| 98 | + |
| 99 | + it.effect("fails createProject before writing files when the selected GitHub token is invalid", () => |
| 100 | + withTempDir((root) => |
| 101 | + Effect.gen(function*(_) { |
| 102 | + const fs = yield* _(FileSystem.FileSystem) |
| 103 | + const path = yield* _(Path.Path) |
| 104 | + const outDir = path.join(root, "project") |
| 105 | + const command = makeCommand(root, outDir, path) |
| 106 | + const fetchMock = vi.fn<typeof globalThis.fetch>(() => |
| 107 | + Effect.runPromise(Effect.succeed(new Response(null, { status: 401 }))) |
| 108 | + ) |
| 109 | + |
| 110 | + yield* _(fs.makeDirectory(path.join(root, ".orch", "env"), { recursive: true })) |
| 111 | + yield* _( |
| 112 | + fs.writeFileString( |
| 113 | + command.config.envGlobalPath, |
| 114 | + [ |
| 115 | + "# docker-git env", |
| 116 | + "GITHUB_TOKEN=dead-token", |
| 117 | + "" |
| 118 | + ].join("\n") |
| 119 | + ) |
| 120 | + ) |
| 121 | + |
| 122 | + const error = yield* _( |
| 123 | + withPatchedFetch( |
| 124 | + fetchMock, |
| 125 | + createProject(command).pipe(Effect.flip) |
| 126 | + ) |
| 127 | + ) |
| 128 | + |
| 129 | + expect(error._tag).toBe("AuthError") |
| 130 | + expect(error.message).toBe(githubInvalidTokenMessage) |
| 131 | + expect(fetchMock).toHaveBeenCalledTimes(1) |
| 132 | + |
| 133 | + const outDirExists = yield* _(fs.exists(outDir)) |
| 134 | + expect(outDirExists).toBe(false) |
| 135 | + }) |
| 136 | + ).pipe(Effect.provide(NodeContext.layer))) |
| 137 | +}) |
0 commit comments