1import * as AWS from "@/AWS";
2import {
3 cacheClusterConnectEnvPrefix,
4 replicationGroupConnectEnvPrefix,
5} from "@/AWS/ElastiCache";
6import * as Test from "@/Test/Alchemy";
7import * as Lambda from "@distilled.cloud/aws/lambda";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import * as Redacted from "effect/Redacted";
11import * as Schedule from "effect/Schedule";
12import * as HttpClient from "effect/http/HttpClient";
13import ProvisionedCacheDataPlaneLive, {
14 ProvisionedCacheDataPlaneFunction,
15} from "./Provisioned.DataPlane.handler.ts";
16import { shareProvisionedNetwork } from "./ProvisionedFixture.ts";
17
18const { test, beforeAll, afterAll } = Test.make({
19 providers: AWS.providers(),
20});
21shareProvisionedNetwork({ beforeAll, afterAll });
22
23// A type alias so it stays comparable to `response.json`'s JsonObject.
24type ConnectionProbe = {
25 valkey: {
26 host: string;
27 port: number;
28 readerHost?: string;
29 readerPort?: number;
30 tls: boolean;
31 };
32 memcached: {
33 endpoints: Array<{ address: string; port: number }>;
34 tls: boolean;
35 };
36};
37
38test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
39 "connects from a VPC Lambda to provisioned Valkey and Memcached",
40 (stack) =>
41 Effect.gen(function* () {
42 yield* stack.destroy();
43 const { fn } = yield* stack.deploy(
44 Effect.gen(function* () {
45 const fn = yield* ProvisionedCacheDataPlaneFunction;
46 return { fn };
47 }).pipe(Effect.provide(ProvisionedCacheDataPlaneLive)),
48 );
49 const baseUrl = fn.functionUrl?.replace(/\/+$/, "");
50 expect(baseUrl).toBeDefined();
51 const config = yield* Lambda.getFunctionConfiguration({
52 FunctionName: fn.functionName,
53 });
54 const env = config.Environment?.Variables ?? {};
55 const envValue = (key: string) => {
56 const value = env[key];
57 return value === undefined || typeof value === "string"
58 ? value
59 : Redacted.value(value);
60 };
61 const valkeyPrefix = replicationGroupConnectEnvPrefix("Valkey");
62 expect(envValue(`${valkeyPrefix}_HOST`)).toBeTruthy();
63 expect(envValue(`${valkeyPrefix}_PORT`)).toBe("6379");
64 expect(envValue(`${valkeyPrefix}_TLS`)).toBe("false");
65 const memcachedPrefix = cacheClusterConnectEnvPrefix("Memcached");
66 expect(envValue(`${memcachedPrefix}_TLS`)).toBe("false");
67 expect(
68 JSON.parse(envValue(`${memcachedPrefix}_ENDPOINTS`) ?? "[]"),
69 ).toHaveLength(1);
70
71 const getJson = (path: string, times: number) =>
72 HttpClient.get(`${baseUrl}${path}`).pipe(
73 Effect.flatMap((response) =>
74 response.status === 200
75 ? response.json
76 : response.text.pipe(
77 Effect.flatMap((body) =>
78 Effect.fail(
79 new Error(`${path} returned ${response.status}: ${body}`),
80 ),
81 ),
82 ),
83 ),
84 Effect.retry({
85 schedule: Schedule.max([
86 Schedule.fixed("3 seconds"),
87 Schedule.recurs(times),
88 ]),
89 }),
90 );
91
92 const connection = (yield* getJson("/connection", 60)) as ConnectionProbe;
93 expect(connection.valkey.host).toBeTruthy();
94 expect(connection.valkey.port).toBe(6379);
95 expect(connection.memcached.endpoints).toHaveLength(1);
96 expect(connection.memcached.endpoints[0]?.port).toBe(11211);
97
98 const valkey = yield* getJson("/valkey?value=hello-valkey", 10);
99 expect((valkey as { value: string }).value).toBe("hello-valkey");
100 const memcached = yield* getJson("/memcached?value=hello-memcached", 10);
101 expect((memcached as { value: string }).value).toBe("hello-memcached");
102
103 yield* stack.destroy();
104 }),
105 {
106 tags: [
107 "provider:aws",
108 "provider:aws:ec2",
109 "provider:aws:elasticache",
110 "provider:aws:lambda",
111 "live",
112 ],
113 timeout: 2_700_000,
114 },
115);