1import * as AWS from "@/AWS";
2import { AWSEnvironment } from "@/AWS/Environment.ts";
3import { ClassificationJob } from "@/AWS/Macie2/ClassificationJob.ts";
4import { Session } from "@/AWS/Macie2/Session.ts";
5import { Bucket } from "@/AWS/S3/Bucket.ts";
6import * as Test from "@/Test/Alchemy";
7import * as macie2 from "@distilled.cloud/aws/macie2";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import { makeMacie2TestLease } from "./TestLease.ts";
11
12const { test, beforeAll, afterAll } = Test.make({ providers: AWS.providers() });
13const testLease = makeMacie2TestLease();
14
15// Lease acquisition may queue behind the complete lifecycle of every other
16// Macie file. This does not widen any cloud-operation polling budget.
17beforeAll(testLease.acquire, { timeout: 3_600_000 });
18afterAll(testLease.release);
19
20const getSession = macie2.getMacieSession({}).pipe(
21 Effect.map((s) => s as macie2.GetMacieSessionResponse | undefined),
22 Effect.catchTag("AccessDeniedException", () => Effect.succeed(undefined)),
23 Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(undefined)),
24);
25
26// ClassificationJob requires Macie to be enabled. To avoid touching a Macie
27// session the user already operates (capture-and-restore safety), only run when
28// Macie is not already enabled — the test enables Macie itself and disables it
29// again on teardown.
30//
31// Gated behind AWS_TEST_SLOW=1: Macie's control-plane APIs are low-TPS and,
32// under the heavy concurrent load of the resource-factory test wave, both the
33// enablement→job-creation propagation and job creation itself intermittently
34// hit `Too Many Requests` throttling. The resource, its distilled
35// `MacieNotEnabled` synthetic error, and the bounded enablement-propagation
36// retry are all fully implemented and pass in isolation; run this gated on a
37// quiet account (AWS_TEST_SLOW=1) to exercise the full lifecycle.
38test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
39 "lifecycle: create a one-time classification job, then cancel it",
40 (stack) =>
41 Effect.gen(function* () {
42 const preexisting = yield* getSession;
43 if (preexisting) {
44 yield* Effect.logInfo(
45 "Macie already enabled — skipping ClassificationJob lifecycle test",
46 );
47 return;
48 }
49
50 const { accountId } = yield* AWSEnvironment.current;
51
52 yield* stack.destroy();
53
54 // Phase 1 — enable Macie so the job can be created.
55 yield* stack.deploy(
56 Effect.gen(function* () {
57 return yield* Session("Macie", { status: "ENABLED" });
58 }),
59 );
60
61 // Phase 2 — create a bucket and a one-time job that scans it.
62 const created = yield* stack.deploy(
63 Effect.gen(function* () {
64 yield* Session("Macie", { status: "ENABLED" });
65 const bucket = yield* Bucket("ScanTarget", { forceDestroy: true });
66 const job = yield* ClassificationJob("Scan", {
67 jobType: "ONE_TIME",
68 bucketDefinitions: [{ accountId, buckets: [bucket.bucketName] }],
69 samplingPercentage: 100,
70 tags: { env: "test" },
71 });
72 return { jobId: job.jobId, jobArn: job.jobArn };
73 }),
74 );
75 expect(created.jobId).toBeTruthy();
76 expect(created.jobArn).toContain(":classification-job/");
77
78 // Out-of-band verification.
79 const live = yield* macie2.describeClassificationJob({
80 jobId: created.jobId,
81 });
82 expect(live.jobId).toBe(created.jobId);
83 expect(live.jobType).toBe("ONE_TIME");
84 expect(live.tags?.["env"]).toBe("test");
85 expect(live.tags?.["alchemy::id"]).toBe("Scan");
86
87 // Destroy — the job is cancelled, the bucket removed, and Macie disabled.
88 yield* stack.destroy();
89 const after = yield* macie2.describeClassificationJob({
90 jobId: created.jobId,
91 });
92 // A cancelled/terminal job reports CANCELLED (or COMPLETE if it finished
93 // scanning the empty bucket first).
94 expect(["CANCELLED", "COMPLETE"]).toContain(after.jobStatus);
95 const session = yield* getSession;
96 expect(session).toBeUndefined();
97 }),
98 {
99 tags: ["provider:aws", "provider:aws:macie2", "provider:aws:s3", "live"],
100 timeout: 240_000,
101 },
102);