1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as efs from "@distilled.cloud/aws/efs";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7
8const { test } = Test.make({ providers: AWS.providers() });
9
10const efsTagsToRecord = (
11 tags: readonly efs.Tag[] | undefined,
12): Record<string, string> =>
13 Object.fromEntries((tags ?? []).map((t) => [t.Key, t.Value]));
14
15// Typed wait-until-gone for an access point.
16const waitUntilAccessPointGone = (accessPointId: string) =>
17 efs.describeAccessPoints({ AccessPointId: accessPointId }).pipe(
18 Effect.map((r) =>
19 (r.AccessPoints ?? []).every((ap) => ap.LifeCycleState === "deleted"),
20 ),
21 Effect.catchTag("AccessPointNotFound", () => Effect.succeed(true)),
22 Effect.repeat({
23 schedule: Schedule.fixed("2 seconds"),
24 until: (gone) => gone,
25 times: 30,
26 }),
27 );
28
29test.provider(
30 "create, update tags, replace, delete access point",
31 (stack) =>
32 Effect.gen(function* () {
33 yield* stack.destroy();
34
35 const infra = (posixUid: number, tag: string) =>
36 Effect.gen(function* () {
37 const files = yield* AWS.EFS.FileSystem("ApFiles");
38 const accessPoint = yield* AWS.EFS.AccessPoint("ApAccess", {
39 fileSystemId: files.fileSystemId,
40 posixUser: { uid: posixUid, gid: 1000 },
41 rootDirectory: {
42 path: "/app",
43 creationInfo: {
44 ownerUid: posixUid,
45 ownerGid: 1000,
46 permissions: "750",
47 },
48 },
49 tags: { purpose: tag },
50 });
51 return { files, accessPoint };
52 });
53
54 // --- create ---
55 const created = yield* stack.deploy(infra(1000, "alchemy-efs-ap-test"));
56 expect(created.accessPoint.accessPointId).toMatch(/^fsap-/);
57 expect(created.accessPoint.accessPointArn).toContain(":access-point/");
58 expect(created.accessPoint.fileSystemId).toBe(created.files.fileSystemId);
59
60 const observed = yield* efs
61 .describeAccessPoints({
62 AccessPointId: created.accessPoint.accessPointId,
63 })
64 .pipe(Effect.map((r) => r.AccessPoints![0]));
65 expect(observed.LifeCycleState).toBe("available");
66 expect(observed.PosixUser?.Uid).toBe(1000);
67 expect(observed.RootDirectory?.Path).toBe("/app");
68 const observedTags = efsTagsToRecord(observed.Tags);
69 expect(observedTags.purpose).toBe("alchemy-efs-ap-test");
70 expect(observedTags["alchemy::id"]).toBe("ApAccess");
71
72 // --- update tags in place ---
73 const retagged = yield* stack.deploy(infra(1000, "alchemy-efs-ap-retag"));
74 expect(retagged.accessPoint.accessPointId).toBe(
75 created.accessPoint.accessPointId,
76 );
77 const retaggedObserved = yield* efs
78 .describeAccessPoints({
79 AccessPointId: created.accessPoint.accessPointId,
80 })
81 .pipe(Effect.map((r) => efsTagsToRecord(r.AccessPoints![0].Tags)));
82 expect(retaggedObserved.purpose).toBe("alchemy-efs-ap-retag");
83
84 // --- posixUser change replaces the access point ---
85 const replaced = yield* stack.deploy(infra(1001, "alchemy-efs-ap-retag"));
86 expect(replaced.accessPoint.accessPointId).not.toBe(
87 created.accessPoint.accessPointId,
88 );
89 const replacedObserved = yield* efs
90 .describeAccessPoints({
91 AccessPointId: replaced.accessPoint.accessPointId,
92 })
93 .pipe(Effect.map((r) => r.AccessPoints![0]));
94 expect(replacedObserved.PosixUser?.Uid).toBe(1001);
95 const oldGone = yield* waitUntilAccessPointGone(
96 created.accessPoint.accessPointId,
97 );
98 expect(oldGone).toBe(true);
99
100 // --- destroy: access point and file system both gone ---
101 yield* stack.destroy();
102 const apGone = yield* waitUntilAccessPointGone(
103 replaced.accessPoint.accessPointId,
104 );
105 expect(apGone).toBe(true);
106 const fsGone = yield* efs
107 .describeFileSystems({ FileSystemId: created.files.fileSystemId })
108 .pipe(
109 Effect.map((r) =>
110 (r.FileSystems ?? []).every((f) => f.LifeCycleState === "deleted"),
111 ),
112 Effect.catchTag("FileSystemNotFound", () => Effect.succeed(true)),
113 Effect.repeat({
114 schedule: Schedule.fixed("2 seconds"),
115 until: (gone) => gone,
116 times: 30,
117 }),
118 );
119 expect(fsGone).toBe(true);
120 }).pipe(
121 Effect.tap(() => stack.destroy()),
122 Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
123 ),
124 { tags: ["provider:aws", "provider:aws:efs", "live"], timeout: 120_000 },
125);