1import * as AWS from "@/AWS";
2import { Policy, PolicyStore, PolicyTemplate } from "@/AWS/VerifiedPermissions";
3import * as Test from "@/Test/Alchemy";
4import * as avp from "@distilled.cloud/aws/verifiedpermissions";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Redacted from "effect/Redacted";
8
9const unwrap = (v: string | Redacted.Redacted<string> | undefined) =>
10 v === undefined ? undefined : Redacted.isRedacted(v) ? Redacted.value(v) : v;
11
12const { test } = Test.make({ providers: AWS.providers() });
13
14const findTemplate = (policyStoreId: string, policyTemplateId: string) =>
15 avp
16 .getPolicyTemplate({ policyStoreId, policyTemplateId })
17 .pipe(
18 Effect.catchTag("ResourceNotFoundException", () =>
19 Effect.succeed(undefined),
20 ),
21 );
22
23const findPolicy = (policyStoreId: string, policyId: string) =>
24 avp
25 .getPolicy({ policyStoreId, policyId })
26 .pipe(
27 Effect.catchTag("ResourceNotFoundException", () =>
28 Effect.succeed(undefined),
29 ),
30 );
31
32const templateStatement = `permit(
33 principal == ?principal,
34 action == PhotoApp::Action::"viewPhoto",
35 resource
36);`;
37
38test.provider(
39 "policy template lifecycle: create template + template-linked policy, update description, destroy",
40 (stack) =>
41 Effect.gen(function* () {
42 yield* stack.destroy();
43
44 const makeStack = (description: string) =>
45 Effect.gen(function* () {
46 const store = yield* PolicyStore("Store", {
47 validationMode: "OFF",
48 });
49 const template = yield* PolicyTemplate("ViewPhoto", {
50 policyStoreId: store.policyStoreId,
51 statement: templateStatement,
52 description,
53 });
54 const linked = yield* Policy("AliceCanView", {
55 policyStoreId: store.policyStoreId,
56 templateId: template.policyTemplateId,
57 principal: { entityType: "PhotoApp::User", entityId: "alice" },
58 });
59 return { store, template, linked };
60 });
61
62 // create
63 const { store, template, linked } = yield* stack.deploy(
64 makeStack("initial description"),
65 );
66 expect(template.policyTemplateId).toBeDefined();
67 expect(linked.policyId).toBeDefined();
68
69 // out-of-band verify
70 const created = yield* findTemplate(
71 store.policyStoreId,
72 template.policyTemplateId,
73 );
74 expect(unwrap(created?.statement)).toContain("?principal");
75 expect(unwrap(created?.description)).toBe("initial description");
76
77 const createdLinked = yield* findPolicy(
78 store.policyStoreId,
79 linked.policyId,
80 );
81 expect(createdLinked?.policyType).toBe("TEMPLATE_LINKED");
82
83 // update the template description in place (ids are stable)
84 const updated = yield* stack.deploy(makeStack("updated description"));
85 expect(updated.template.policyTemplateId).toBe(template.policyTemplateId);
86 expect(updated.linked.policyId).toBe(linked.policyId);
87
88 const afterUpdate = yield* findTemplate(
89 store.policyStoreId,
90 template.policyTemplateId,
91 );
92 expect(unwrap(afterUpdate?.description)).toBe("updated description");
93
94 // destroy — linked policy must delete before the template
95 yield* stack.destroy();
96 const goneTemplate = yield* findTemplate(
97 store.policyStoreId,
98 template.policyTemplateId,
99 );
100 expect(goneTemplate).toBeUndefined();
101 }),
102 {
103 tags: ["provider:aws", "provider:aws:verifiedpermissions", "live"],
104 timeout: 180_000,
105 },
106);