1import * as CostAndUsageReport from "@/AWS/CostAndUsageReport";
2import * as Lambda from "@/AWS/Lambda";
3import { Bucket } from "@/AWS/S3";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import path from "pathe";
10
11const main = path.resolve(import.meta.dirname, "handler.ts");
12
13// Deterministic names — distinct from ReportDefinition.test.ts's fixtures so
14// the two suites never collide.
15export const REPORT_NAME = "alchemy-test-cur-bindings-report";
16export const BUCKET_NAME = "alchemy-test-cur-bindings";
17
18export class CurTestFunction extends Lambda.Function<Lambda.Function>()(
19 "CurTestFunction",
20) {}
21
22export default CurTestFunction.make(
23 {
24 main,
25 functionUrl: true,
26 timeout: Duration.seconds(30),
27 },
28 Effect.gen(function* () {
29 // The delivery bucket policy AWS validates before accepting a report
30 // definition (billingreports.amazonaws.com needs GetBucketAcl/
31 // GetBucketPolicy on the bucket and PutObject under it).
32 const bucket = yield* Bucket("CurBindingsBucket", {
33 bucketName: BUCKET_NAME,
34 forceDestroy: true,
35 policy: [
36 {
37 Effect: "Allow",
38 Principal: { Service: "billingreports.amazonaws.com" },
39 Action: ["s3:GetBucketAcl", "s3:GetBucketPolicy"],
40 Resource: `arn:aws:s3:::${BUCKET_NAME}`,
41 },
42 {
43 Effect: "Allow",
44 Principal: { Service: "billingreports.amazonaws.com" },
45 Action: ["s3:PutObject"],
46 Resource: `arn:aws:s3:::${BUCKET_NAME}/*`,
47 },
48 ],
49 });
50
51 const report = yield* CostAndUsageReport.ReportDefinition(
52 "CurBindingsReport",
53 {
54 reportName: REPORT_NAME,
55 timeUnit: "DAILY",
56 format: "textORcsv",
57 compression: "GZIP",
58 s3Bucket: bucket.bucketName,
59 s3Prefix: "cur-bindings",
60 s3Region: bucket.region,
61 tags: { fixture: "cur-bindings" },
62 },
63 );
64
65 // --- account-level bindings ---
66 const describeReportDefinitions =
67 yield* CostAndUsageReport.DescribeReportDefinitions();
68
69 // --- report-scoped bindings ---
70 const listReportTags =
71 yield* CostAndUsageReport.ListTagsForResource(report);
72
73 const bound = {
74 describeReportDefinitions,
75 listReportTags,
76 };
77
78 return {
79 fetch: Effect.gen(function* () {
80 const request = yield* HttpServerRequest;
81 const url = new URL(request.originalUrl);
82 const pathname = url.pathname;
83
84 if (request.method === "GET" && pathname === "/bindings") {
85 return yield* HttpServerResponse.json({
86 bound: Object.keys(bound),
87 });
88 }
89
90 if (request.method === "GET" && pathname === "/reports") {
91 const result = yield* describeReportDefinitions();
92 const names = (result.ReportDefinitions ?? []).map(
93 (r) => r.ReportName,
94 );
95 return yield* HttpServerResponse.json({
96 count: names.length,
97 names,
98 });
99 }
100
101 if (request.method === "GET" && pathname === "/report-tags") {
102 const result = yield* listReportTags();
103 return yield* HttpServerResponse.json({
104 tags: Object.fromEntries(
105 (result.Tags ?? []).map((t) => [t.Key, t.Value]),
106 ),
107 });
108 }
109
110 return yield* HttpServerResponse.json(
111 { error: "Not found", method: request.method, pathname },
112 { status: 404 },
113 );
114 }).pipe(Effect.orDie),
115 };
116 }).pipe(
117 Effect.provide(
118 Layer.mergeAll(
119 CostAndUsageReport.DescribeReportDefinitionsHttp,
120 CostAndUsageReport.ListTagsForResourceHttp,
121 ),
122 ),
123 ),
124);