1import * as AWS from "@/AWS";
2import { IdentitySource, PolicyStore } 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 findSource = (policyStoreId: string, identitySourceId: string) =>
15 avp
16 .getIdentitySource({ policyStoreId, identitySourceId })
17 .pipe(
18 Effect.catchTag("ResourceNotFoundException", () =>
19 Effect.succeed(undefined),
20 ),
21 );
22
23test.provider(
24 "identity source lifecycle: create OIDC source, update client ids, destroy",
25 (stack) =>
26 Effect.gen(function* () {
27 yield* stack.destroy();
28
29 const makeStack = (clientIds: string[]) =>
30 Effect.gen(function* () {
31 const store = yield* PolicyStore("Store", {
32 validationMode: "OFF",
33 });
34 const source = yield* IdentitySource("Oidc", {
35 policyStoreId: store.policyStoreId,
36 principalEntityType: "PhotoApp::User",
37 openIdConnect: {
38 // AVP fetches /.well-known/openid-configuration at create time,
39 // so the issuer must be a live OIDC discovery endpoint
40 issuer: "https://accounts.google.com",
41 tokenSelection: {
42 identityTokenOnly: { clientIds },
43 },
44 },
45 });
46 return { store, source };
47 });
48
49 // create
50 const { store, source } = yield* stack.deploy(
51 makeStack(["alchemy-test-client"]),
52 );
53 expect(source.identitySourceId).toBeDefined();
54 expect(source.policyStoreId).toBe(store.policyStoreId);
55
56 // out-of-band verify
57 const created = yield* findSource(
58 store.policyStoreId,
59 source.identitySourceId,
60 );
61 expect(unwrap(created?.principalEntityType)).toBe("PhotoApp::User");
62 expect(created?.configuration?.openIdConnectConfiguration?.issuer).toBe(
63 "https://accounts.google.com",
64 );
65
66 // update client ids in place (identitySourceId is stable)
67 const updated = yield* stack.deploy(
68 makeStack(["alchemy-test-client", "second-client"]),
69 );
70 expect(updated.source.identitySourceId).toBe(source.identitySourceId);
71
72 const afterUpdate = yield* findSource(
73 store.policyStoreId,
74 source.identitySourceId,
75 );
76 const tokenSelection =
77 afterUpdate?.configuration?.openIdConnectConfiguration?.tokenSelection;
78 const observedClientIds = (
79 tokenSelection?.identityTokenOnly?.clientIds ?? []
80 ).map((id) => unwrap(id)!);
81 expect([...observedClientIds].sort()).toEqual([
82 "alchemy-test-client",
83 "second-client",
84 ]);
85
86 // destroy
87 yield* stack.destroy();
88 const gone = yield* findSource(
89 store.policyStoreId,
90 source.identitySourceId,
91 );
92 expect(gone).toBeUndefined();
93 }),
94 {
95 tags: ["provider:aws", "provider:aws:verifiedpermissions", "live"],
96 timeout: 180_000,
97 },
98);