1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import {
4 AuthArchiveReadScope,
5 AuthArchiveWriteScope,
6 AuthOrchestrationReadScope,
7 AuthOrchestrationOperateScope,
8} from "@t3tools/contracts";
9import * as ConfigProvider from "effect/ConfigProvider";
10import * as Crypto from "effect/Crypto";
11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
13import * as Layer from "effect/Layer";
14import { HttpRouter } from "effect/http";
15import * as ServerConfig from "../config.ts";
16import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
17import { layerArchiveUploadRoute, layerArchiveDownloadRoute } from "../http.ts";
18import * as ArchiveObjects from "./ArchiveObjects.ts";
19
20it.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);