1import * as AWS from "@/AWS";
2import { Profile } from "@/AWS/B2BI";
3import * as Test from "@/Test/Alchemy";
4import * as b2bi from "@distilled.cloud/aws/b2bi";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Ungated typed-error probe: proves the distilled error union carries the
12// not-found tag this provider's read/delete paths depend on.
13test.provider(
14 "getProfile on a nonexistent id fails with ResourceNotFoundException",
15 () =>
16 Effect.gen(function* () {
17 const error = yield* Effect.flip(
18 b2bi.getProfile({ profileId: "p-00000000000000000" }),
19 );
20 expect(["ResourceNotFoundException", "ValidationException"]).toContain(
21 error._tag,
22 );
23 }),
24 { tags: ["provider:aws", "provider:aws:b2bi", "live"] },
25);
26
27const assertProfileGone = (profileId: string) =>
28 Effect.gen(function* () {
29 const result = yield* b2bi.getProfile({ profileId }).pipe(
30 Effect.map(() => "present" as const),
31 Effect.catchTag("ResourceNotFoundException", () =>
32 Effect.succeed("gone" as const),
33 ),
34 );
35 if (result === "present") {
36 return yield* Effect.fail(
37 new Error(`Profile '${profileId}' still exists`),
38 );
39 }
40 }).pipe(
41 Effect.retry({
42 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(8)]),
43 }),
44 );
45
46// Profiles are credential-free and cheap, so the full lifecycle runs ungated.
47test.provider(
48 "create, update, and destroy a B2BI profile",
49 (stack) =>
50 Effect.gen(function* () {
51 yield* stack.destroy();
52
53 // Create.
54 const created = yield* stack.deploy(
55 Profile("Acme", {
56 name: "alchemy-b2bi-profile",
57 businessName: "Alchemy Test Corp",
58 phone: "+15555550100",
59 email: "edi@alchemy.example",
60 }),
61 );
62 expect(created.profileId).toMatch(/^p-/);
63 expect(created.name).toBe("alchemy-b2bi-profile");
64 expect(created.businessName).toBe("Alchemy Test Corp");
65 expect(created.profileArn).toContain(":b2bi:");
66
67 // Out-of-band verification.
68 const described = yield* b2bi.getProfile({
69 profileId: created.profileId,
70 });
71 expect(described.businessName).toBe("Alchemy Test Corp");
72
73 // No-op redeploy keeps the same id.
74 const noop = yield* stack.deploy(
75 Profile("Acme", {
76 name: "alchemy-b2bi-profile",
77 businessName: "Alchemy Test Corp",
78 phone: "+15555550100",
79 email: "edi@alchemy.example",
80 }),
81 );
82 expect(noop.profileId).toBe(created.profileId);
83
84 // Update businessName and phone in place.
85 const updated = yield* stack.deploy(
86 Profile("Acme", {
87 name: "alchemy-b2bi-profile",
88 businessName: "Alchemy Renamed Corp",
89 phone: "+15555550199",
90 email: "edi@alchemy.example",
91 }),
92 );
93 expect(updated.profileId).toBe(created.profileId);
94 expect(updated.businessName).toBe("Alchemy Renamed Corp");
95 const reDescribed = yield* b2bi.getProfile({
96 profileId: created.profileId,
97 });
98 expect(reDescribed.businessName).toBe("Alchemy Renamed Corp");
99
100 // Changing logging replaces the profile (no member on updateProfile);
101 // delete-first because profiles are recovered by name and quota-capped.
102 const replaced = yield* stack.deploy(
103 Profile("Acme", {
104 name: "alchemy-b2bi-profile",
105 businessName: "Alchemy Renamed Corp",
106 phone: "+15555550199",
107 email: "edi@alchemy.example",
108 logging: "DISABLED",
109 }),
110 );
111 expect(replaced.profileId).not.toBe(created.profileId);
112 yield* assertProfileGone(created.profileId);
113
114 // Destroy and verify.
115 yield* stack.destroy();
116 yield* assertProfileGone(replaced.profileId);
117 }),
118 { tags: ["provider:aws", "provider:aws:b2bi", "live"], timeout: 120_000 },
119);