1import * as AWS from "@/AWS";
2import { AWSEnvironment } from "@/AWS/Environment.ts";
3import { DataSource } from "@/AWS/QuickSight";
4import * as Test from "@/Test/Alchemy";
5import * as quicksight from "@distilled.cloud/aws/quicksight";
6import { expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// The testing account has no QuickSight subscription, so the full lifecycle
14// is gated behind AWS_TEST_QUICKSIGHT=1 (run on an entitled account). The
15// ungated probe below proves both the distilled patch and the gating.
16const SUBSCRIBED = !!process.env.AWS_TEST_QUICKSIGHT;
17
18/**
19 * Ungated probe: without an account subscription, QuickSight rejects
20 * `CreateDataSource` with `ResourceNotFoundException` whose message is
21 * "Directory information for account ... is not found." The distilled patch
22 * specializes that into the typed `QuickSightSubscriptionRequired` tag. This
23 * test asserts the tag is what surfaces (or, on an entitled account, that the
24 * probe data source was created and is cleaned up).
25 */
26test.provider(
27 "createDataSource surfaces typed QuickSightSubscriptionRequired without a subscription",
28 () =>
29 Effect.gen(function* () {
30 const { accountId } = yield* AWSEnvironment.current;
31 const dataSourceId = "alchemy-quicksight-subscription-probe";
32
33 const outcome = yield* quicksight
34 .createDataSource({
35 AwsAccountId: accountId,
36 DataSourceId: dataSourceId,
37 Name: "AlchemyQuickSightSubscriptionProbe",
38 Type: "ATHENA",
39 DataSourceParameters: {
40 AthenaParameters: { WorkGroup: "primary" },
41 },
42 })
43 .pipe(
44 Effect.as("created" as const),
45 Effect.catchTag("QuickSightSubscriptionRequired", (e) =>
46 Effect.succeed(e._tag),
47 ),
48 // Tolerate a leftover probe from a prior entitled run.
49 Effect.catchTag("ResourceExistsException", () =>
50 Effect.succeed("created" as const),
51 ),
52 );
53
54 // Clean up if the account turned out to be subscribed.
55 if (outcome === "created") {
56 yield* quicksight
57 .deleteDataSource({
58 AwsAccountId: accountId,
59 DataSourceId: dataSourceId,
60 })
61 .pipe(
62 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
63 );
64 }
65
66 expect(["QuickSightSubscriptionRequired", "created"]).toContain(outcome);
67 }),
68 {
69 tags: ["provider:aws", "provider:aws:quicksight", "live"],
70 timeout: 60_000,
71 },
72);
73
74class DataSourceStillExists extends Data.TaggedError("DataSourceStillExists")<{
75 readonly dataSourceId: string;
76}> {}
77
78const findDataSource = (accountId: string, dataSourceId: string) =>
79 quicksight
80 .describeDataSource({ AwsAccountId: accountId, DataSourceId: dataSourceId })
81 .pipe(
82 Effect.map((r) => r.DataSource),
83 Effect.catchTag("ResourceNotFoundException", () =>
84 Effect.succeed(undefined),
85 ),
86 );
87
88// Gated lifecycle: requires an entitled QuickSight account.
89test.provider.skipIf(!SUBSCRIBED)(
90 "create Athena data source, verify out-of-band, delete",
91 (stack) =>
92 Effect.gen(function* () {
93 const { accountId } = yield* AWSEnvironment.current;
94 yield* stack.destroy();
95
96 const { source } = yield* stack.deploy(
97 Effect.gen(function* () {
98 const source = yield* DataSource("AthenaSource", {
99 name: "Alchemy Athena Source",
100 type: "ATHENA",
101 dataSourceParameters: {
102 AthenaParameters: { WorkGroup: "primary" },
103 },
104 });
105 return { source };
106 }),
107 );
108
109 expect(source.dataSourceId).toBeDefined();
110 expect(source.arn).toContain(":datasource/");
111
112 const observed = yield* findDataSource(accountId, source.dataSourceId);
113 expect(observed?.Arn).toBe(source.arn);
114 expect(observed?.Type).toBe("ATHENA");
115
116 // internal ownership tags applied
117 const tags = yield* quicksight
118 .listTagsForResource({ ResourceArn: source.arn })
119 .pipe(Effect.map((r) => r.Tags ?? []));
120 expect(tags.find((t) => t.Key === "alchemy::id")?.Value).toBe(
121 "AthenaSource",
122 );
123
124 yield* stack.destroy();
125
126 yield* findDataSource(accountId, source.dataSourceId).pipe(
127 Effect.flatMap((ds) =>
128 ds === undefined || ds.Status === "DELETED"
129 ? Effect.void
130 : Effect.fail(
131 new DataSourceStillExists({
132 dataSourceId: source.dataSourceId,
133 }),
134 ),
135 ),
136 Effect.retry({
137 while: (e) => e._tag === "DataSourceStillExists",
138 schedule: Schedule.max([
139 Schedule.exponential(500),
140 Schedule.recurs(8),
141 ]),
142 }),
143 );
144 }),
145 {
146 tags: ["provider:aws", "provider:aws:quicksight", "live"],
147 timeout: 180_000,
148 },
149);