1import * as AWS from "@/AWS";
2import {
3 Domain,
4 Environment,
5 EnvironmentBlueprintConfiguration,
6 Project,
7} from "@/AWS/DataZone";
8import * as IAM from "@/AWS/IAM";
9import * as S3 from "@/AWS/S3";
10import * as Test from "@/Test/Alchemy";
11import * as datazone from "@distilled.cloud/aws/datazone";
12import { expect } from "alchemy-test";
13import * as Effect from "effect/Effect";
14import * as Result from "effect/Result";
15
16const { test } = Test.make({ providers: AWS.providers() });
17
18// Environment deployment drives a CloudFormation stack (several minutes each
19// way) — the full lifecycle only runs when explicitly requested.
20const RUN_SLOW = process.env.AWS_TEST_SLOW === "1";
21
22// The DefaultDataLake blueprint requires an S3 location whose bucket name
23// contains "datazone" (the provisioning policy scopes S3 access to
24// *datazone* buckets). Deterministic constant — never Date.now().
25const DATALAKE_BUCKET = "amazon-datazone-391965393224-usw2-alchemy-test";
26
27// Ungated: the typed error union covers the not-found probe — proves the
28// distilled error mapping without provisioning anything.
29test.provider(
30 "getEnvironment on a nonexistent domain is typed",
31 () =>
32 Effect.gen(function* () {
33 const result = yield* datazone
34 .getEnvironment({
35 domainIdentifier: "dzd_000000000000",
36 identifier: "0000000000",
37 })
38 .pipe(Effect.result);
39 expect(Result.isFailure(result)).toBe(true);
40 if (Result.isFailure(result)) {
41 expect([
42 "ResourceNotFoundException",
43 "AccessDeniedException",
44 "ValidationException",
45 ]).toContain(result.failure._tag);
46 }
47 }),
48 { tags: ["provider:aws", "provider:aws:datazone", "live"] },
49);
50
51/** Domain + roles + bucket + blueprint config + project shared by both deploys. */
52const baseInfra = Effect.gen(function* () {
53 const domain = yield* Domain("EnvTestDomain", {
54 description: "alchemy datazone environment test",
55 });
56 const provisioningRole = yield* IAM.Role("EnvProvisioningRole", {
57 assumeRolePolicyDocument: {
58 Version: "2012-10-17",
59 Statement: [
60 {
61 Effect: "Allow",
62 Principal: { Service: "cloudformation.amazonaws.com" },
63 Action: ["sts:AssumeRole"],
64 },
65 ],
66 },
67 managedPolicyArns: [
68 "arn:aws:iam::aws:policy/AmazonDataZoneRedshiftGlueProvisioningPolicy",
69 ],
70 });
71 const manageAccessRole = yield* IAM.Role("EnvManageAccessRole", {
72 assumeRolePolicyDocument: {
73 Version: "2012-10-17",
74 Statement: [
75 {
76 Effect: "Allow",
77 Principal: { Service: "datazone.amazonaws.com" },
78 Action: ["sts:AssumeRole", "sts:TagSession"],
79 },
80 ],
81 },
82 managedPolicyArns: [
83 "arn:aws:iam::aws:policy/service-role/AmazonDataZoneGlueManageAccessRolePolicy",
84 ],
85 });
86 const bucket = yield* S3.Bucket("EnvDataLakeBucket", {
87 bucketName: DATALAKE_BUCKET,
88 forceDestroy: true,
89 });
90 const config = yield* EnvironmentBlueprintConfiguration(
91 "EnvDataLakeBlueprint",
92 {
93 domainId: domain.domainId,
94 environmentBlueprint: "DefaultDataLake",
95 enabledRegions: ["us-west-2"],
96 provisioningRoleArn: provisioningRole.roleArn,
97 manageAccessRoleArn: manageAccessRole.roleArn,
98 regionalParameters: {
99 "us-west-2": { S3Location: `s3://${DATALAKE_BUCKET}` },
100 },
101 },
102 );
103 const project = yield* Project("EnvTestProject", {
104 domainId: domain.domainId,
105 description: "environment test project",
106 });
107 return { domain, config, project };
108});
109
110test.provider.skipIf(!RUN_SLOW)(
111 "create environment from the DefaultDataLake blueprint, delete",
112 (stack) =>
113 Effect.gen(function* () {
114 yield* stack.destroy();
115
116 // 1. Deploy the domain, project, roles, bucket, and blueprint config.
117 const base = yield* stack.deploy(
118 Effect.gen(function* () {
119 const { domain, config, project } = yield* baseInfra;
120 return {
121 domainId: domain.domainId,
122 projectId: project.projectId,
123 environmentBlueprintId: config.environmentBlueprintId,
124 };
125 }),
126 );
127
128 // 2. Create an environment profile out-of-band (no alchemy resource
129 // for it yet) and deploy the Environment from it.
130 const profile = yield* datazone.createEnvironmentProfile({
131 domainIdentifier: base.domainId,
132 name: "alchemy-env-test-profile",
133 environmentBlueprintIdentifier: base.environmentBlueprintId,
134 projectIdentifier: base.projectId,
135 awsAccountId: "391965393224",
136 awsAccountRegion: "us-west-2",
137 });
138
139 const result = yield* stack
140 .deploy(
141 Effect.gen(function* () {
142 const { domain, project } = yield* baseInfra;
143 const env = yield* Environment("TestEnvironment", {
144 domainId: domain.domainId,
145 projectId: project.projectId,
146 environmentProfileId: profile.id,
147 description: "alchemy environment",
148 });
149 return {
150 environmentId: env.environmentId,
151 status: env.status,
152 };
153 }),
154 )
155 .pipe(
156 Effect.ensuring(
157 // the environment profile blocks domain deletion — always remove
158 // it before the final destroy, even when the deploy fails.
159 datazone
160 .deleteEnvironmentProfile({
161 domainIdentifier: base.domainId,
162 identifier: profile.id,
163 })
164 .pipe(Effect.ignore),
165 ),
166 );
167
168 expect(result.environmentId).toBeDefined();
169 expect(result.status).toBe("ACTIVE");
170
171 // out-of-band: the environment is ACTIVE.
172 const created = yield* datazone.getEnvironment({
173 domainIdentifier: base.domainId,
174 identifier: result.environmentId,
175 });
176 expect(created.status).toBe("ACTIVE");
177 expect(created.projectId).toBe(base.projectId);
178
179 // 3. Destroy — environment, project, blueprint config, and domain.
180 yield* stack.destroy();
181 const gone = yield* datazone
182 .getEnvironment({
183 domainIdentifier: base.domainId,
184 identifier: result.environmentId,
185 })
186 .pipe(
187 Effect.map((env) => env.status === "DELETED"),
188 Effect.catchTag("ResourceNotFoundException", () =>
189 Effect.succeed(true),
190 ),
191 // once the domain is deleted, DataZone reports AccessDenied (auth
192 // is checked before existence) — also gone.
193 Effect.catchTag("AccessDeniedException", () => Effect.succeed(true)),
194 );
195 expect(gone).toBe(true);
196 }),
197 {
198 tags: [
199 "provider:aws",
200 "provider:aws:datazone",
201 "provider:aws:iam",
202 "provider:aws:s3",
203 "live",
204 ],
205 timeout: 1_100_000,
206 },
207);