1import * as AWS from "@/AWS";
2import { Endpoint } from "@/AWS/DMS";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as dms from "@distilled.cloud/aws/database-migration-service";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Redacted from "effect/Redacted";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13const findEndpoint = (identifier: string) =>
14 dms
15 .describeEndpoints({
16 Filters: [{ Name: "endpoint-id", Values: [identifier] }],
17 })
18 .pipe(
19 Effect.map((r) => r.Endpoints?.[0]),
20 Effect.catchTag("ResourceNotFoundFault", () => Effect.succeed(undefined)),
21 );
22
23// Deletion is verified as gone or at least initiated (status `deleting`,
24// which is irreversible). Full disappearance is eventually consistent.
25const assertGone = (identifier: string) =>
26 findEndpoint(identifier).pipe(
27 Effect.map((endpoint) => endpoint?.Status ?? "gone"),
28 Effect.flatMap((status) =>
29 status === "gone" || status === "deleting"
30 ? Effect.void
31 : Effect.fail(new Error(`endpoint '${identifier}' status: ${status}`)),
32 ),
33 Effect.retry({
34 schedule: Schedule.max([
35 Schedule.fixed("5 seconds"),
36 Schedule.recurs(24),
37 ]),
38 }),
39 );
40
41test.provider(
42 "create, update, delete a DMS source endpoint",
43 (stack) =>
44 Effect.gen(function* () {
45 yield* stack.destroy();
46
47 // Create a MySQL source endpoint. DMS endpoints are metadata-only, so
48 // no live database is required.
49 const { endpoint } = yield* stack.deploy(
50 Effect.gen(function* () {
51 const endpoint = yield* Endpoint("Source", {
52 endpointType: "source",
53 engineName: "mysql",
54 serverName: "source-db.example.com",
55 port: 3306,
56 username: "admin",
57 password: Redacted.make("correct-horse-battery-staple"),
58 databaseName: "app",
59 tags: { team: "data" },
60 });
61 return { endpoint };
62 }),
63 );
64
65 // DMS echoes the endpoint type back uppercased.
66 expect(endpoint.endpointType).toBe("SOURCE");
67 expect(endpoint.engineName).toBe("mysql");
68 expect(endpoint.endpointArn).toContain(":endpoint:");
69 expect(endpoint.tags.team).toBe("data");
70
71 // Out-of-band verification via distilled.
72 const observed = yield* findEndpoint(endpoint.endpointIdentifier);
73 expect(observed?.EndpointArn).toBe(endpoint.endpointArn);
74 expect(observed?.ServerName).toBe("source-db.example.com");
75 expect(observed?.Port).toBe(3306);
76
77 // Update: change port + username and one tag.
78 const { endpoint: updated } = yield* stack.deploy(
79 Effect.gen(function* () {
80 const endpoint = yield* Endpoint("Source", {
81 endpointType: "source",
82 engineName: "mysql",
83 serverName: "source-db.example.com",
84 port: 3307,
85 username: "readonly",
86 databaseName: "app",
87 tags: { team: "platform" },
88 });
89 return { endpoint };
90 }),
91 );
92
93 // Same endpoint (in-place modify).
94 expect(updated.endpointArn).toBe(endpoint.endpointArn);
95 const observed2 = yield* findEndpoint(endpoint.endpointIdentifier);
96 expect(observed2?.Port).toBe(3307);
97 expect(observed2?.Username).toBe("readonly");
98 expect(updated.tags.team).toBe("platform");
99
100 yield* stack.destroy();
101 yield* assertGone(endpoint.endpointIdentifier);
102 }),
103 { tags: ["provider:aws", "provider:aws:dms", "live"], timeout: 240_000 },
104);
105
106test.provider(
107 "list enumerates the deployed endpoint",
108 (stack) =>
109 Effect.gen(function* () {
110 yield* stack.destroy();
111
112 const { endpoint } = yield* stack.deploy(
113 Effect.gen(function* () {
114 const endpoint = yield* Endpoint("ListTarget", {
115 endpointType: "target",
116 engineName: "postgres",
117 serverName: "target-db.example.com",
118 port: 5432,
119 username: "admin",
120 password: Redacted.make("hunter2"),
121 databaseName: "warehouse",
122 });
123 return { endpoint };
124 }),
125 );
126
127 const provider = yield* Provider.findProvider(Endpoint);
128 const all = yield* provider.list();
129 expect(all.some((e) => e.endpointArn === endpoint.endpointArn)).toBe(
130 true,
131 );
132
133 yield* stack.destroy();
134 yield* assertGone(endpoint.endpointIdentifier);
135 }),
136 { tags: ["provider:aws", "provider:aws:dms", "live"], timeout: 240_000 },
137);