1import * as AWS from "@/AWS";
2import { Cluster, ClusterPolicy } from "@/AWS/DSQL";
3import * as Test from "@/Test/Alchemy";
4import * as dsql from "@distilled.cloud/aws/dsql";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7
8const { test } = Test.make({ providers: AWS.providers() });
9
10/** Docs-canonical policy: deny non-VPC connections. */
11const vpcOnlyPolicy = (exceptions?: string[]) =>
12 JSON.stringify({
13 Version: "2012-10-17",
14 Statement: [
15 {
16 Sid: "DenyNonVpcConnect",
17 Effect: "Deny",
18 Principal: { AWS: "*" },
19 Action: ["dsql:DbConnect", "dsql:DbConnectAdmin"],
20 Resource: "*",
21 Condition: {
22 Null: { "aws:SourceVpc": "true" },
23 ...(exceptions
24 ? { StringNotEquals: { "aws:PrincipalArn": exceptions } }
25 : {}),
26 },
27 },
28 ],
29 });
30
31const getCluster = (identifier: string) =>
32 dsql
33 .getCluster({ identifier })
34 .pipe(
35 Effect.catchTag("ResourceNotFoundException", () =>
36 Effect.succeed(undefined),
37 ),
38 );
39
40test.provider(
41 "create, update deletion protection, delete DSQL cluster",
42 (stack) =>
43 Effect.gen(function* () {
44 yield* stack.destroy();
45
46 // create (deletion protection off for test economics)
47 const created = yield* stack.deploy(
48 Effect.gen(function* () {
49 return yield* Cluster("AppDb", {
50 tags: { app: "alchemy-test" },
51 });
52 }),
53 );
54
55 expect(created.clusterId).toBeDefined();
56 expect(created.clusterArn).toContain(`:cluster/${created.clusterId}`);
57 expect(["ACTIVE", "IDLE"]).toContain(created.status);
58 expect(created.endpoint).toContain(created.clusterId);
59 expect(created.deletionProtectionEnabled).toBe(false);
60
61 // out-of-band verification
62 const observed = yield* getCluster(created.clusterId);
63 expect(observed?.identifier).toEqual(created.clusterId);
64 expect(observed?.deletionProtectionEnabled).toBe(false);
65
66 // update: enable deletion protection
67 const updated = yield* stack.deploy(
68 Effect.gen(function* () {
69 return yield* Cluster("AppDb", {
70 deletionProtectionEnabled: true,
71 tags: { app: "alchemy-test" },
72 });
73 }),
74 );
75 expect(updated.clusterId).toEqual(created.clusterId);
76 const reobserved = yield* getCluster(created.clusterId);
77 expect(reobserved?.deletionProtectionEnabled).toBe(true);
78
79 // attach a resource-based cluster policy (singleton sub-resource)
80 const withPolicy = yield* stack.deploy(
81 Effect.gen(function* () {
82 const cluster = yield* Cluster("AppDb", {
83 deletionProtectionEnabled: true,
84 tags: { app: "alchemy-test" },
85 });
86 const policy = yield* ClusterPolicy("AppDbPolicy", {
87 clusterId: cluster.clusterId,
88 policy: vpcOnlyPolicy(),
89 });
90 return {
91 clusterId: cluster.clusterId,
92 policyVersion: policy.policyVersion,
93 };
94 }),
95 );
96 expect(withPolicy.clusterId).toEqual(created.clusterId);
97 expect(withPolicy.policyVersion).toBeDefined();
98
99 // out-of-band verification of the attached document
100 const attached = yield* dsql.getClusterPolicy({
101 identifier: created.clusterId,
102 });
103 expect(attached.policy).toContain("DenyNonVpcConnect");
104 expect(attached.policyVersion).toEqual(withPolicy.policyVersion);
105
106 // update the policy document in place (version bumps)
107 const policyUpdated = yield* stack.deploy(
108 Effect.gen(function* () {
109 const cluster = yield* Cluster("AppDb", {
110 deletionProtectionEnabled: true,
111 tags: { app: "alchemy-test" },
112 });
113 const policy = yield* ClusterPolicy("AppDbPolicy", {
114 clusterId: cluster.clusterId,
115 policy: vpcOnlyPolicy([
116 "arn:aws:iam::123456789012:role/ExceptionRole",
117 ]),
118 });
119 return { policyVersion: policy.policyVersion };
120 }),
121 );
122 expect(policyUpdated.policyVersion).not.toEqual(withPolicy.policyVersion);
123 const reattached = yield* dsql.getClusterPolicy({
124 identifier: created.clusterId,
125 });
126 expect(reattached.policy).toContain("ExceptionRole");
127
128 // delete (provider disables deletion protection automatically)
129 yield* stack.destroy();
130 const gone = yield* getCluster(created.clusterId);
131 // A deleted DSQL cluster is either gone or reports DELETING/DELETED.
132 expect(
133 gone === undefined ||
134 gone.status === "DELETING" ||
135 gone.status === "DELETED",
136 ).toBe(true);
137 }),
138 { tags: ["provider:aws", "provider:aws:dsql", "live"], timeout: 300_000 },
139);