| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { assert, it } from "@effect/vitest"; |
| 3 | import { |
| 4 | AuthArchiveReadScope, |
| 5 | AuthArchiveWriteScope, |
| 6 | AuthOrchestrationReadScope, |
| 7 | AuthOrchestrationOperateScope, |
| 8 | } from "@t3tools/contracts"; |
| 9 | import * as ConfigProvider from "effect/ConfigProvider"; |
| 10 | import * as Crypto from "effect/Crypto"; |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as FileSystem from "effect/FileSystem"; |
| 13 | import * as Layer from "effect/Layer"; |
| 14 | import { HttpRouter } from "effect/http"; |
| 15 | import * as ServerConfig from "../config.ts"; |
| 16 | import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; |
| 17 | import { layerArchiveUploadRoute, layerArchiveDownloadRoute } from "../http.ts"; |
| 18 | import * as ArchiveObjects from "./ArchiveObjects.ts"; |
| 19 | |
| 20 | it.live( |
| 21 | "requires archive scopes, acknowledges verified bytes, and serves authenticated byte ranges", |
| 22 | () => |
| 23 | Effect.gen(function* () { |
| 24 | const fs = yield* FileSystem.FileSystem; |
| 25 | const root = yield* fs.makeTempDirectoryScoped({ prefix: "knife-archive-http-" }); |
| 26 | const layers = Layer.mergeAll(ArchiveObjects.layer, EnvironmentAuth.layerRuntime).pipe( |
| 27 | Layer.provideMerge( |
| 28 | ServerConfig.layerTest(process.cwd(), { prefix: "knife-archive-http-state-" }), |
| 29 | ), |
| 30 | Layer.provide( |
| 31 | ConfigProvider.layer(ConfigProvider.fromUnknown({ T3CODE_ARCHIVE_DIR: root })), |
| 32 | ), |
| 33 | ); |
| 34 | yield* Effect.gen(function* () { |
| 35 | const auth = yield* EnvironmentAuth.EnvironmentAuth; |
| 36 | const read = yield* auth.issueSession({ scopes: [AuthArchiveReadScope] }); |
| 37 | const legacy = yield* auth.issueSession({ |
| 38 | scopes: [AuthOrchestrationReadScope, AuthOrchestrationOperateScope], |
| 39 | }); |
| 40 | const write = yield* auth.issueSession({ scopes: [AuthArchiveWriteScope] }); |
| 41 | const context = yield* Effect.context< |
| 42 | EnvironmentAuth.EnvironmentAuth | ArchiveObjects.ArchiveObjects | FileSystem.FileSystem |
| 43 | >(); |
| 44 | const app = yield* Effect.acquireRelease( |
| 45 | Effect.sync(() => |
| 46 | HttpRouter.toWebHandler( |
| 47 | Layer.merge(layerArchiveUploadRoute, layerArchiveDownloadRoute), |
| 48 | { disableLogger: true }, |
| 49 | ), |
| 50 | ), |
| 51 | (app) => Effect.promise(() => app.dispose()), |
| 52 | ); |
| 53 | const bytes = new TextEncoder().encode("0123456789"); |
| 54 | const hash = Buffer.from(yield* (yield* Crypto.Crypto).digest("SHA-256", bytes)).toString( |
| 55 | "hex", |
| 56 | ); |
| 57 | const request = (method: string, token?: string, range?: string, objectHash = hash) => |
| 58 | Effect.promise(() => |
| 59 | app.handler( |
| 60 | new Request(`http://127.0.0.1/api/archive/objects/${objectHash}`, { |
| 61 | method, |
| 62 | headers: { |
| 63 | ...(token ? { authorization: `Bearer ${token}` } : {}), |
| 64 | ...(range ? { range } : {}), |
| 65 | }, |
| 66 | ...(method === "PUT" ? { body: bytes } : {}), |
| 67 | }), |
| 68 | context, |
| 69 | ), |
| 70 | ); |
| 71 | assert.strictEqual((yield* request("PUT")).status, 401); |
| 72 | assert.strictEqual((yield* request("PUT", read.token)).status, 403); |
| 73 | assert.strictEqual((yield* request("PUT", legacy.token)).status, 200); |
| 74 | assert.strictEqual((yield* request("GET", legacy.token)).status, 200); |
| 75 | const uploaded = yield* request("PUT", write.token); |
| 76 | assert.strictEqual(uploaded.status, 200); |
| 77 | assert.deepStrictEqual(yield* Effect.promise(() => uploaded.json()), { hash, size: 10 }); |
| 78 | assert.strictEqual( |
| 79 | (yield* request("PUT", write.token, undefined, "0".repeat(64))).status, |
| 80 | 400, |
| 81 | ); |
| 82 | assert.strictEqual((yield* request("GET", write.token)).status, 403); |
| 83 | const partial = yield* request("GET", read.token, "bytes=3-6"); |
| 84 | assert.strictEqual(partial.status, 206); |
| 85 | assert.strictEqual(partial.headers.get("content-range"), "bytes 3-6/10"); |
| 86 | assert.strictEqual(yield* Effect.promise(() => partial.text()), "3456"); |
| 87 | assert.strictEqual((yield* request("GET", read.token, "bytes=20-")).status, 416); |
| 88 | const full = yield* request("GET", read.token); |
| 89 | assert.strictEqual(yield* Effect.promise(() => full.text()), "0123456789"); |
| 90 | }).pipe(Effect.provide(layers)); |
| 91 | }).pipe(Effect.provide(NodeServices.layer)), |
| 92 | ); |