1import * as AWS from "@/AWS";
2import { ContactInformation } 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 captureContact = () =>
20 account.getContactInformation({}).pipe(
21 Effect.map((r) => r.ContactInformation),
22 Effect.catchTag("ResourceNotFoundException", () =>
23 Effect.succeed(undefined),
24 ),
25 );
26
27// getContactInformation is eventually consistent after putContactInformation,
28// so out-of-band reads poll (bounded) until the expected company name shows.
29const captureContactUntil = (expectedCompany: string | undefined) =>
30 captureContact().pipe(
31 Effect.repeat({
32 schedule: Schedule.spaced("3 seconds"),
33 until: (contact): boolean =>
34 unwrap(contact?.CompanyName) === expectedCompany,
35 times: 10,
36 }),
37 );
38
39// AWS validates the phone number for pattern validity (e.g. NANP numbers
40// with an invalid area code like 555, or an exchange starting with 1, are
41// rejected with `ValidationException: The specified PhoneNumber is not
42// valid`). Reuse the account's current primary-contact phone when one exists
43// — it is guaranteed to pass — and fall back to a pattern-valid number.
44const FALLBACK_PHONE = "+12025550100";
45
46// The primary contact is an account-global singleton that cannot be deleted.
47// This test captures whatever primary contact the account currently has, sets
48// its own, updates it, and restores the original in `ensuring` so the account
49// is left exactly as it was found.
50describe.sequential(
51 "Account ContactInformation",
52 { tags: ["provider:aws", "provider:aws:account", "live"] },
53 () => {
54 test.provider(
55 "set, update, and restore the primary contact",
56 (stack) =>
57 Effect.gen(function* () {
58 yield* stack.destroy();
59
60 const original = yield* captureContact();
61 const testPhone = unwrap(original?.PhoneNumber) ?? FALLBACK_PHONE;
62
63 const restore = Effect.gen(function* () {
64 if (original) {
65 yield* account.putContactInformation({
66 ContactInformation: original,
67 });
68 }
69 });
70
71 yield* Effect.gen(function* () {
72 const deployContact = (companyName: string) =>
73 stack.deploy(
74 Effect.gen(function* () {
75 return yield* ContactInformation("PrimaryContact", {
76 fullName: "Alchemy Test",
77 addressLine1: "123 Any Street",
78 city: "Seattle",
79 stateOrRegion: "WA",
80 postalCode: "98101",
81 countryCode: "US",
82 phoneNumber: testPhone,
83 companyName,
84 });
85 }),
86 );
87
88 const contact = yield* deployContact("Alchemy");
89 expect(contact.fullName).toBe("Alchemy Test");
90 expect(contact.companyName).toBe("Alchemy");
91
92 // Out-of-band verification (bounded poll through put→get eventual
93 // consistency).
94 const live = yield* captureContactUntil("Alchemy");
95 expect(unwrap(live?.FullName)).toBe("Alchemy Test");
96 expect(unwrap(live?.CompanyName)).toBe("Alchemy");
97
98 // Update: the singleton is upserted in place.
99 const updated = yield* deployContact("Alchemy Updated");
100 expect(updated.companyName).toBe("Alchemy Updated");
101 const liveUpdated = yield* captureContactUntil("Alchemy Updated");
102 expect(unwrap(liveUpdated?.CompanyName)).toBe("Alchemy Updated");
103
104 // Destroy is a no-op — AWS does not allow deleting the primary
105 // contact, so the last value stays in place.
106 yield* stack.destroy();
107 const after = yield* captureContactUntil("Alchemy Updated");
108 expect(unwrap(after?.FullName)).toBe("Alchemy Test");
109 }).pipe(Effect.ensuring(restore.pipe(Effect.orDie)));
110
111 // Confirm restoration matches the captured original (bounded poll
112 // through put→get eventual consistency).
113 const restored = yield* captureContact().pipe(
114 Effect.repeat({
115 schedule: Schedule.spaced("3 seconds"),
116 until: (contact): boolean =>
117 unwrap(contact?.FullName) === unwrap(original?.FullName),
118 times: 10,
119 }),
120 );
121 expect(unwrap(restored?.FullName)).toBe(unwrap(original?.FullName));
122 }),
123 { timeout: 120_000 },
124 );
125 },
126);