1import * as AWS from "@/AWS";
2import { AccountName } from "@/AWS/Account";
3import * as Test from "@/Test/Alchemy";
4import * as account from "@distilled.cloud/aws/account";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Redacted from "effect/Redacted";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12const unwrap = (value: string | Redacted.Redacted<string> | undefined) =>
13 value === undefined
14 ? undefined
15 : typeof value === "string"
16 ? value
17 : Redacted.value(value);
18
19const TEST_NAME = "alchemy-test-account-name";
20
21// getAccountInformation is eventually consistent after putAccountName (the
22// rename can take ~20s to become visible), so out-of-band reads poll
23// (bounded) until the expected name is observed.
24const readNameUntil = (expected: string | undefined) =>
25 account.getAccountInformation({}).pipe(
26 Effect.map((info) => unwrap(info.AccountName)),
27 Effect.repeat({
28 schedule: Schedule.spaced("3 seconds"),
29 until: (name): boolean => name === expected,
30 times: 10,
31 }),
32 );
33
34// The account name is an account-global singleton. This test captures the
35// current name, renames the account to a marker, verifies out-of-band,
36// destroys (a no-op — an account always has a name), and restores the
37// original name so the account is left exactly as it was found.
38describe.sequential(
39 "Account AccountName",
40 { tags: ["provider:aws", "provider:aws:account", "live"] },
41 () => {
42 test.provider(
43 "set, read, and restore the account name",
44 (stack) =>
45 Effect.gen(function* () {
46 yield* stack.destroy();
47
48 const info = yield* account.getAccountInformation({});
49 const originalName = unwrap(info.AccountName);
50
51 const restore = Effect.gen(function* () {
52 if (originalName !== undefined) {
53 yield* account.putAccountName({ AccountName: originalName });
54 }
55 });
56
57 yield* Effect.gen(function* () {
58 const name = yield* stack.deploy(
59 Effect.gen(function* () {
60 return yield* AccountName("Name", {
61 accountName: TEST_NAME,
62 });
63 }),
64 );
65
66 expect(name.accountName).toBe(TEST_NAME);
67 expect(name.accountId).toBe(info.AccountId);
68
69 // Out-of-band verification (bounded poll through the rename's
70 // eventual consistency).
71 const live = yield* readNameUntil(TEST_NAME);
72 expect(live).toBe(TEST_NAME);
73
74 // Destroy is a no-op — an account always has a name, so the last
75 // value stays in place.
76 yield* stack.destroy();
77 const after = yield* readNameUntil(TEST_NAME);
78 expect(after).toBe(TEST_NAME);
79 }).pipe(Effect.ensuring(restore.pipe(Effect.orDie)));
80
81 // Confirm restoration matches the captured original.
82 const restored = yield* readNameUntil(originalName);
83 expect(restored).toBe(originalName);
84 }),
85 { timeout: 120_000 },
86 );
87 },
88);