| 1 | import * as AWS from "@/AWS"; |
| 2 | import { AWSEnvironment } from "@/AWS/Environment.ts"; |
| 3 | import { DataSource } from "@/AWS/QuickSight"; |
| 4 | import * as Test from "@/Test/Alchemy"; |
| 5 | import * as quicksight from "@distilled.cloud/aws/quicksight"; |
| 6 | import { expect } from "alchemy-test"; |
| 7 | import * as Data from "effect/Data"; |
| 8 | import * as Effect from "effect/Effect"; |
| 9 | import * as Schedule from "effect/Schedule"; |
| 10 | |
| 11 | const { 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. |
| 16 | const 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 | */ |
| 26 | test.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 | |
| 74 | class DataSourceStillExists extends Data.TaggedError("DataSourceStillExists")<{ |
| 75 | readonly dataSourceId: string; |
| 76 | }> {} |
| 77 | |
| 78 | const 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. |
| 89 | test.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 | ); |