| ... | ... | @@ -18,6 +18,10 @@ import { |
| 18 | 18 | makeCloudflaredRelayClient, |
| 19 | 19 | } from "./relayClient.ts"; |
| 20 | 20 | |
| 21 | // The suite runs the linux code path against the real filesystem, checking |
| 22 | // POSIX exec bits that NTFS never reports; the win32 branch skips that check. |
| 23 | const windowsHost = HostProcessPlatform.defaultValue() === "win32"; |
| 24 | |
| 21 | 25 | const hostRuntimeLayer = (env: Record<string, string> = {}) => |
| 22 | 26 | Layer.mergeAll( |
| 23 | 27 | Layer.succeed(HostProcessPlatform, "linux"), |
| ... | ... | @@ -63,102 +67,106 @@ const makeSpawnerLayer = (commands: Array<string>) => |
| 63 | 67 | ); |
| 64 | 68 | |
| 65 | 69 | describe("RelayClient", () => { |
| 66 | | it.effect("resolves explicit overrides before managed and PATH executables", () => |
| 67 | | Effect.gen(function* () { |
| 68 | | const fileSystem = yield* FileSystem.FileSystem; |
| 69 | | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ |
| 70 | | prefix: "t3-cloudflared-test-", |
| 71 | | }); |
| 72 | | const overridePath = `${baseDir}/override-cloudflared`; |
| 73 | | yield* fileSystem.writeFileString(overridePath, "override"); |
| 74 | | yield* fileSystem.chmod(overridePath, 0o755); |
| 75 | | const manager = yield* makeCloudflaredRelayClient({ |
| 76 | | baseDir, |
| 77 | | }); |
| 70 | it.effect.skipIf(windowsHost)( |
| 71 | "resolves explicit overrides before managed and PATH executables", |
| 72 | () => |
| 73 | Effect.gen(function* () { |
| 74 | const fileSystem = yield* FileSystem.FileSystem; |
| 75 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ |
| 76 | prefix: "t3-cloudflared-test-", |
| 77 | }); |
| 78 | const overridePath = `${baseDir}/override-cloudflared`; |
| 79 | yield* fileSystem.writeFileString(overridePath, "override"); |
| 80 | yield* fileSystem.chmod(overridePath, 0o755); |
| 81 | const manager = yield* makeCloudflaredRelayClient({ |
| 82 | baseDir, |
| 83 | }); |
| 78 | 84 | |
| 79 | | expect( |
| 80 | | yield* manager.resolve.pipe( |
| 81 | | Effect.provideService( |
| 82 | | ConfigProvider.ConfigProvider, |
| 83 | | ConfigProvider.fromEnv({ |
| 84 | | env: { PATH: "", T3CODE_CLOUDFLARED_PATH: overridePath }, |
| 85 | | }), |
| 85 | expect( |
| 86 | yield* manager.resolve.pipe( |
| 87 | Effect.provideService( |
| 88 | ConfigProvider.ConfigProvider, |
| 89 | ConfigProvider.fromEnv({ |
| 90 | env: { PATH: "", T3CODE_CLOUDFLARED_PATH: overridePath }, |
| 91 | }), |
| 92 | ), |
| 93 | ), |
| 94 | ).toEqual({ |
| 95 | status: "available", |
| 96 | executablePath: overridePath, |
| 97 | source: "override", |
| 98 | version: CLOUDFLARED_VERSION, |
| 99 | }); |
| 100 | }).pipe( |
| 101 | Effect.scoped, |
| 102 | Effect.provide( |
| 103 | Layer.mergeAll( |
| 104 | NodeServices.layer, |
| 105 | makeHttpClientLayer(new Uint8Array()), |
| 106 | makeSpawnerLayer([]), |
| 107 | hostRuntimeLayer(), |
| 86 | 108 | ), |
| 87 | | ), |
| 88 | | ).toEqual({ |
| 89 | | status: "available", |
| 90 | | executablePath: overridePath, |
| 91 | | source: "override", |
| 92 | | version: CLOUDFLARED_VERSION, |
| 93 | | }); |
| 94 | | }).pipe( |
| 95 | | Effect.scoped, |
| 96 | | Effect.provide( |
| 97 | | Layer.mergeAll( |
| 98 | | NodeServices.layer, |
| 99 | | makeHttpClientLayer(new Uint8Array()), |
| 100 | | makeSpawnerLayer([]), |
| 101 | | hostRuntimeLayer(), |
| 102 | 109 | ), |
| 103 | 110 | ), |
| 104 | | ), |
| 105 | 111 | ); |
| 106 | 112 | |
| 107 | | it.effect("downloads, verifies, validates, and atomically installs the managed executable", () => |
| 108 | | Effect.gen(function* () { |
| 109 | | const fileSystem = yield* FileSystem.FileSystem; |
| 110 | | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ |
| 111 | | prefix: "t3-cloudflared-test-", |
| 112 | | }); |
| 113 | | const bytes = new TextEncoder().encode("test-cloudflared-binary"); |
| 114 | | const manager = yield* makeCloudflaredRelayClient({ |
| 115 | | baseDir, |
| 116 | | releaseAsset: { |
| 117 | | url: "https://example.test/cloudflared", |
| 118 | | sha256: Encoding.encodeHex(sha256(bytes)), |
| 119 | | archive: "binary", |
| 120 | | }, |
| 121 | | }); |
| 113 | it.effect.skipIf(windowsHost)( |
| 114 | "downloads, verifies, validates, and atomically installs the managed executable", |
| 115 | () => |
| 116 | Effect.gen(function* () { |
| 117 | const fileSystem = yield* FileSystem.FileSystem; |
| 118 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ |
| 119 | prefix: "t3-cloudflared-test-", |
| 120 | }); |
| 121 | const bytes = new TextEncoder().encode("test-cloudflared-binary"); |
| 122 | const manager = yield* makeCloudflaredRelayClient({ |
| 123 | baseDir, |
| 124 | releaseAsset: { |
| 125 | url: "https://example.test/cloudflared", |
| 126 | sha256: Encoding.encodeHex(sha256(bytes)), |
| 127 | archive: "binary", |
| 128 | }, |
| 129 | }); |
| 122 | 130 | |
| 123 | | const progress: Array<string> = []; |
| 124 | | const installed = yield* manager.installWithProgress((event) => |
| 125 | | Effect.sync(() => { |
| 126 | | if (event.type === "progress") { |
| 127 | | progress.push(event.stage); |
| 128 | | } |
| 129 | | }), |
| 130 | | ); |
| 131 | | const managedPath = `${baseDir}/tools/cloudflared/${CLOUDFLARED_VERSION}/linux-x64/cloudflared`; |
| 132 | | expect(installed).toEqual({ |
| 133 | | status: "available", |
| 134 | | executablePath: managedPath, |
| 135 | | source: "managed", |
| 136 | | version: CLOUDFLARED_VERSION, |
| 137 | | }); |
| 138 | | expect(new TextDecoder().decode(yield* fileSystem.readFile(managedPath))).toBe( |
| 139 | | "test-cloudflared-binary", |
| 140 | | ); |
| 141 | | expect(progress).toEqual([ |
| 142 | | "checking", |
| 143 | | "waiting_for_lock", |
| 144 | | "downloading", |
| 145 | | "verifying", |
| 146 | | "installing", |
| 147 | | "validating", |
| 148 | | "activating", |
| 149 | | ]); |
| 150 | | expect(yield* manager.resolve).toEqual(installed); |
| 151 | | }).pipe( |
| 152 | | Effect.scoped, |
| 153 | | Effect.provide( |
| 154 | | Layer.mergeAll( |
| 155 | | NodeServices.layer, |
| 156 | | makeHttpClientLayer(new TextEncoder().encode("test-cloudflared-binary")), |
| 157 | | makeSpawnerLayer([]), |
| 158 | | hostRuntimeLayer(), |
| 131 | const progress: Array<string> = []; |
| 132 | const installed = yield* manager.installWithProgress((event) => |
| 133 | Effect.sync(() => { |
| 134 | if (event.type === "progress") { |
| 135 | progress.push(event.stage); |
| 136 | } |
| 137 | }), |
| 138 | ); |
| 139 | const managedPath = `${baseDir}/tools/cloudflared/${CLOUDFLARED_VERSION}/linux-x64/cloudflared`; |
| 140 | expect(installed).toEqual({ |
| 141 | status: "available", |
| 142 | executablePath: managedPath, |
| 143 | source: "managed", |
| 144 | version: CLOUDFLARED_VERSION, |
| 145 | }); |
| 146 | expect(new TextDecoder().decode(yield* fileSystem.readFile(managedPath))).toBe( |
| 147 | "test-cloudflared-binary", |
| 148 | ); |
| 149 | expect(progress).toEqual([ |
| 150 | "checking", |
| 151 | "waiting_for_lock", |
| 152 | "downloading", |
| 153 | "verifying", |
| 154 | "installing", |
| 155 | "validating", |
| 156 | "activating", |
| 157 | ]); |
| 158 | expect(yield* manager.resolve).toEqual(installed); |
| 159 | }).pipe( |
| 160 | Effect.scoped, |
| 161 | Effect.provide( |
| 162 | Layer.mergeAll( |
| 163 | NodeServices.layer, |
| 164 | makeHttpClientLayer(new TextEncoder().encode("test-cloudflared-binary")), |
| 165 | makeSpawnerLayer([]), |
| 166 | hostRuntimeLayer(), |
| 167 | ), |
| 159 | 168 | ), |
| 160 | 169 | ), |
| 161 | | ), |
| 162 | 170 | ); |
| 163 | 171 | |
| 164 | 172 | it.effect("rejects downloads whose checksum does not match the pinned manifest", () => |
| ... | ... | @@ -192,7 +200,7 @@ describe("RelayClient", () => { |
| 192 | 200 | ), |
| 193 | 201 | ); |
| 194 | 202 | |
| 195 | | it.effect("serializes concurrent installs within one runtime", () => { |
| 203 | it.effect.skipIf(windowsHost)("serializes concurrent installs within one runtime", () => { |
| 196 | 204 | const commands: Array<string> = []; |
| 197 | 205 | const bytes = new TextEncoder().encode("test-cloudflared-binary"); |
| 198 | 206 | return Effect.gen(function* () { |
| ... | ... | @@ -227,45 +235,48 @@ describe("RelayClient", () => { |
| 227 | 235 | ); |
| 228 | 236 | }); |
| 229 | 237 | |
| 230 | | it.effect("observes PATH changes after the manager has been constructed", () => { |
| 231 | | const env = { PATH: "" }; |
| 232 | | return Effect.gen(function* () { |
| 233 | | const fileSystem = yield* FileSystem.FileSystem; |
| 234 | | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ |
| 235 | | prefix: "t3-cloudflared-test-", |
| 236 | | }); |
| 237 | | const binDir = `${baseDir}/bin`; |
| 238 | | const executablePath = `${binDir}/cloudflared`; |
| 239 | | const manager = yield* makeCloudflaredRelayClient({ |
| 240 | | baseDir, |
| 241 | | }); |
| 238 | it.effect.skipIf(windowsHost)( |
| 239 | "observes PATH changes after the manager has been constructed", |
| 240 | () => { |
| 241 | const env = { PATH: "" }; |
| 242 | return Effect.gen(function* () { |
| 243 | const fileSystem = yield* FileSystem.FileSystem; |
| 244 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ |
| 245 | prefix: "t3-cloudflared-test-", |
| 246 | }); |
| 247 | const binDir = `${baseDir}/bin`; |
| 248 | const executablePath = `${binDir}/cloudflared`; |
| 249 | const manager = yield* makeCloudflaredRelayClient({ |
| 250 | baseDir, |
| 251 | }); |
| 242 | 252 | |
| 243 | | expect(yield* manager.resolve).toEqual({ |
| 244 | | status: "missing", |
| 245 | | version: CLOUDFLARED_VERSION, |
| 246 | | }); |
| 253 | expect(yield* manager.resolve).toEqual({ |
| 254 | status: "missing", |
| 255 | version: CLOUDFLARED_VERSION, |
| 256 | }); |
| 247 | 257 | |
| 248 | | yield* fileSystem.makeDirectory(binDir); |
| 249 | | yield* fileSystem.writeFileString(executablePath, "cloudflared"); |
| 250 | | yield* fileSystem.chmod(executablePath, 0o755); |
| 251 | | env.PATH = binDir; |
| 258 | yield* fileSystem.makeDirectory(binDir); |
| 259 | yield* fileSystem.writeFileString(executablePath, "cloudflared"); |
| 260 | yield* fileSystem.chmod(executablePath, 0o755); |
| 261 | env.PATH = binDir; |
| 252 | 262 | |
| 253 | | expect(yield* manager.resolve).toEqual({ |
| 254 | | status: "available", |
| 255 | | executablePath, |
| 256 | | source: "path", |
| 257 | | version: CLOUDFLARED_VERSION, |
| 258 | | }); |
| 259 | | }).pipe( |
| 260 | | Effect.scoped, |
| 261 | | Effect.provide( |
| 262 | | Layer.mergeAll( |
| 263 | | NodeServices.layer, |
| 264 | | makeHttpClientLayer(new Uint8Array()), |
| 265 | | makeSpawnerLayer([]), |
| 266 | | hostRuntimeLayer(env), |
| 263 | expect(yield* manager.resolve).toEqual({ |
| 264 | status: "available", |
| 265 | executablePath, |
| 266 | source: "path", |
| 267 | version: CLOUDFLARED_VERSION, |
| 268 | }); |
| 269 | }).pipe( |
| 270 | Effect.scoped, |
| 271 | Effect.provide( |
| 272 | Layer.mergeAll( |
| 273 | NodeServices.layer, |
| 274 | makeHttpClientLayer(new Uint8Array()), |
| 275 | makeSpawnerLayer([]), |
| 276 | hostRuntimeLayer(env), |
| 277 | ), |
| 267 | 278 | ), |
| 268 | | ), |
| 269 | | ); |
| 270 | | }); |
| 279 | ); |
| 280 | }, |
| 281 | ); |
| 271 | 282 | }); |