1import * as AWS from "@/AWS";
2import { Portfolio } from "@/AWS/ServiceCatalog";
3import * as Test from "@/Test/Alchemy";
4import * as servicecatalog from "@distilled.cloud/aws/service-catalog";
5import { expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12class PortfolioStillExists extends Data.TaggedError("PortfolioStillExists")<{
13 portfolioId: string;
14}> {}
15
16const assertPortfolioGone = (portfolioId: string) =>
17 servicecatalog.describePortfolio({ Id: portfolioId }).pipe(
18 Effect.flatMap(() =>
19 Effect.fail(new PortfolioStillExists({ portfolioId })),
20 ),
21 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
22 Effect.retry({
23 while: (e) => e._tag === "PortfolioStillExists",
24 schedule: Schedule.max([
25 Schedule.fixed("2 seconds"),
26 Schedule.recurs(10),
27 ]),
28 }),
29 );
30
31test.provider(
32 "creates, updates, and deletes a portfolio",
33 (stack) =>
34 Effect.gen(function* () {
35 yield* stack.destroy();
36
37 const created = yield* stack.deploy(
38 Effect.gen(function* () {
39 const portfolio = yield* Portfolio("TestPortfolio", {
40 providerName: "alchemy-tests",
41 description: "portfolio lifecycle test",
42 tags: { purpose: "lifecycle" },
43 });
44 return {
45 portfolioId: portfolio.portfolioId,
46 portfolioArn: portfolio.portfolioArn,
47 portfolioName: portfolio.portfolioName,
48 };
49 }),
50 );
51 expect(created.portfolioId).toMatch(/^port-/);
52 expect(created.portfolioArn).toContain(":catalog:");
53
54 // out-of-band verify via distilled
55 const described = yield* servicecatalog.describePortfolio({
56 Id: created.portfolioId,
57 });
58 expect(described.PortfolioDetail?.DisplayName).toBe(
59 created.portfolioName,
60 );
61 expect(described.PortfolioDetail?.ProviderName).toBe("alchemy-tests");
62 expect(described.PortfolioDetail?.Description).toBe(
63 "portfolio lifecycle test",
64 );
65 const tags = Object.fromEntries(
66 (described.Tags ?? []).map((t) => [t.Key, t.Value]),
67 );
68 expect(tags.purpose).toBe("lifecycle");
69 expect(tags["alchemy::id"]).toBe("TestPortfolio");
70
71 // in-place update of provider name, description, and tags
72 yield* stack.deploy(
73 Effect.gen(function* () {
74 yield* Portfolio("TestPortfolio", {
75 providerName: "alchemy-tests-updated",
76 description: "updated description",
77 tags: { purpose: "lifecycle-updated", extra: "yes" },
78 });
79 }),
80 );
81 const updated = yield* servicecatalog.describePortfolio({
82 Id: created.portfolioId,
83 });
84 expect(updated.PortfolioDetail?.Id).toBe(created.portfolioId);
85 expect(updated.PortfolioDetail?.ProviderName).toBe(
86 "alchemy-tests-updated",
87 );
88 expect(updated.PortfolioDetail?.Description).toBe("updated description");
89 const updatedTags = Object.fromEntries(
90 (updated.Tags ?? []).map((t) => [t.Key, t.Value]),
91 );
92 expect(updatedTags.purpose).toBe("lifecycle-updated");
93 expect(updatedTags.extra).toBe("yes");
94
95 yield* stack.destroy();
96 yield* assertPortfolioGone(created.portfolioId);
97 }),
98 {
99 tags: ["provider:aws", "provider:aws:servicecatalog", "live"],
100 timeout: 180_000,
101 },
102);