1import * as AWS from "@/AWS";
2import { ExperimentTemplate, TargetAccountConfiguration } from "@/AWS/FIS";
3import { Role } from "@/AWS/IAM/Role.ts";
4import * as Test from "@/Test/Alchemy";
5import * as fis from "@distilled.cloud/aws/fis";
6import * as sts from "@distilled.cloud/aws/sts";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12const findConfiguration = (experimentTemplateId: string, accountId: string) =>
13 fis.getTargetAccountConfiguration({ experimentTemplateId, accountId }).pipe(
14 Effect.map((r) => r.targetAccountConfiguration),
15 Effect.catchTag("ResourceNotFoundException", () =>
16 Effect.succeed(undefined),
17 ),
18 );
19
20test.provider(
21 "create, update, delete target account configuration",
22 (stack) =>
23 Effect.gen(function* () {
24 yield* stack.destroy();
25
26 // Multi-account experiments accept the orchestrator account itself as a
27 // target account, so the whole lifecycle runs single-account.
28 const { Account } = yield* sts.getCallerIdentity({});
29 const accountId = Account!;
30
31 const program = (description: string) =>
32 Effect.gen(function* () {
33 const role = yield* Role("FisTargetAccountRole", {
34 assumeRolePolicyDocument: {
35 Version: "2012-10-17",
36 Statement: [
37 {
38 Effect: "Allow",
39 Principal: { Service: "fis.amazonaws.com" },
40 Action: ["sts:AssumeRole"],
41 },
42 ],
43 },
44 });
45 const template = yield* ExperimentTemplate("MultiAccountTemplate", {
46 roleArn: role.roleArn,
47 experimentOptions: { accountTargeting: "multi-account" },
48 actions: {
49 Wait: {
50 actionId: "aws:fis:wait",
51 parameters: { duration: "PT1M" },
52 },
53 },
54 });
55 const config = yield* TargetAccountConfiguration("TargetAccount", {
56 experimentTemplateId: template.id,
57 accountId,
58 roleArn: role.roleArn,
59 description,
60 });
61 return { template, config };
62 });
63
64 const { template, config } = yield* stack.deploy(
65 program("alchemy fis tac test"),
66 );
67 expect(config.experimentTemplateId).toBe(template.id);
68 expect(config.accountId).toBe(accountId);
69 expect(config.roleArn).toContain(":role/");
70
71 // out-of-band verification via distilled
72 const created = yield* findConfiguration(template.id, accountId);
73 expect(created).toBeDefined();
74 expect(created?.accountId).toBe(accountId);
75 expect(created?.roleArn).toBe(config.roleArn);
76 expect(created?.description).toBe("alchemy fis tac test");
77
78 // update in place — same identity, new description
79 const updated = yield* stack.deploy(
80 program("alchemy fis tac test updated"),
81 );
82 expect(updated.config.experimentTemplateId).toBe(template.id);
83 expect(updated.config.accountId).toBe(accountId);
84
85 const afterUpdate = yield* findConfiguration(template.id, accountId);
86 expect(afterUpdate?.description).toBe("alchemy fis tac test updated");
87
88 yield* stack.destroy();
89 const afterDestroy = yield* findConfiguration(template.id, accountId);
90 expect(afterDestroy).toBeUndefined();
91 }),
92 {
93 tags: ["provider:aws", "provider:aws:fis", "provider:aws:iam", "live"],
94 timeout: 120_000,
95 },
96);