1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import { describe, expect } from "alchemy-test";
5import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import * as HttpClientRequest from "effect/http/HttpClientRequest";
10import GeoRoutesTestFunctionLive, { GeoRoutesTestFunction } from "./handler.ts";
11
12const testOptions = { providers: AWS.providers() };
13const { test, beforeAll, afterAll } = Test.make(testOptions);
14const sharedStack = Core.scratchStack(testOptions, "GeoRoutesBindings");
15
16const readinessPolicy = Schedule.max([
17 Schedule.fixed("2 seconds"),
18 Schedule.recurs(75),
19]);
20
21let baseUrl: string;
22
23class TransientUpstream extends Data.TaggedError("TransientUpstream")<{
24 readonly status: number;
25 readonly body: string;
26}> {}
27
28const send = (request: HttpClientRequest.HttpClientRequest) =>
29 HttpClient.execute(request).pipe(
30 Effect.flatMap((response) =>
31 response.status >= 500
32 ? response.text.pipe(
33 Effect.flatMap((body) =>
34 Effect.fail(
35 new TransientUpstream({ status: response.status, body }),
36 ),
37 ),
38 )
39 : Effect.succeed(response),
40 ),
41 Effect.retry({
42 while: (e) => e._tag === "TransientUpstream",
43 schedule: Schedule.max([
44 Schedule.exponential("1 second"),
45 Schedule.recurs(5),
46 ]),
47 }),
48 );
49
50describe(
51 "GeoRoutes Bindings",
52 {
53 tags: [
54 "provider:aws",
55 "provider:aws:georoutes",
56 "provider:aws:lambda",
57 "live",
58 ],
59 },
60 () => {
61 beforeAll(
62 Effect.gen(function* () {
63 yield* Effect.logInfo(
64 "GeoRoutes test setup: destroying previous resources",
65 );
66 yield* sharedStack.destroy();
67
68 yield* Effect.logInfo("GeoRoutes test setup: deploying fixture");
69 const { functionUrl } = yield* sharedStack.deploy(
70 Effect.gen(function* () {
71 return yield* GeoRoutesTestFunction;
72 }).pipe(Effect.provide(GeoRoutesTestFunctionLive)),
73 );
74
75 expect(functionUrl).toBeTruthy();
76 baseUrl = functionUrl!.replace(/\/+$/, "");
77 const readinessUrl = `${baseUrl}/ping`;
78
79 yield* Effect.logInfo(
80 `GeoRoutes test setup: probing readiness at ${readinessUrl}`,
81 );
82 yield* HttpClient.get(readinessUrl).pipe(
83 Effect.flatMap((response) =>
84 response.status === 200
85 ? Effect.succeed(response)
86 : Effect.fail(
87 new Error(`Function not ready: ${response.status}`),
88 ),
89 ),
90 Effect.tapError((error) =>
91 Effect.logWarning(
92 `GeoRoutes test setup: fixture not ready yet (${String(error)})`,
93 ),
94 ),
95 Effect.retry({ schedule: readinessPolicy }),
96 );
97 }),
98 { timeout: 240_000 },
99 );
100
101 afterAll(sharedStack.destroy(), { timeout: 120_000 });
102
103 describe("GeoRoutes.CalculateRoutes", () => {
104 test.provider(
105 "computes a route between two points",
106 (_stack) =>
107 Effect.gen(function* () {
108 const response = (yield* send(
109 HttpClientRequest.get(`${baseUrl}/calculate-routes`),
110 ).pipe(Effect.flatMap((r) => r.json))) as {
111 count: number;
112 distance?: number;
113 duration?: number;
114 };
115
116 expect(response.count).toBeGreaterThan(0);
117 expect(response.distance).toBeGreaterThan(0);
118 expect(response.duration).toBeGreaterThan(0);
119 }),
120 { timeout: 120_000 },
121 );
122 });
123
124 describe("GeoRoutes.CalculateIsolines", () => {
125 test.provider(
126 "computes a drive-time isoline around a point",
127 (_stack) =>
128 Effect.gen(function* () {
129 const response = (yield* send(
130 HttpClientRequest.get(`${baseUrl}/calculate-isolines`),
131 ).pipe(Effect.flatMap((r) => r.json))) as {
132 count: number;
133 geometryCount: number;
134 pricingBucket?: string;
135 };
136
137 expect(response.count).toBeGreaterThan(0);
138 expect(response.geometryCount).toBeGreaterThan(0);
139 expect(typeof response.pricingBucket).toBe("string");
140 }),
141 { timeout: 120_000 },
142 );
143 });
144
145 describe("GeoRoutes.CalculateRouteMatrix", () => {
146 test.provider(
147 "computes a 2x2 origin-destination matrix",
148 (_stack) =>
149 Effect.gen(function* () {
150 const response = (yield* send(
151 HttpClientRequest.get(`${baseUrl}/calculate-route-matrix`),
152 ).pipe(Effect.flatMap((r) => r.json))) as {
153 rows: number;
154 columns: number;
155 firstDuration?: number;
156 errorCount: number;
157 pricingBucket?: string;
158 };
159
160 expect(response.rows).toBe(2);
161 expect(response.columns).toBe(2);
162 expect(response.errorCount).toBe(0);
163 expect(response.firstDuration).toBeGreaterThan(0);
164 expect(typeof response.pricingBucket).toBe("string");
165 }),
166 { timeout: 120_000 },
167 );
168 });
169
170 describe("GeoRoutes.OptimizeWaypoints", () => {
171 test.provider(
172 "orders waypoints to minimize travel time",
173 (_stack) =>
174 Effect.gen(function* () {
175 const response = (yield* send(
176 HttpClientRequest.get(`${baseUrl}/optimize-waypoints`),
177 ).pipe(Effect.flatMap((r) => r.json))) as {
178 optimizedCount: number;
179 order: string[];
180 distance?: number;
181 duration?: number;
182 pricingBucket?: string;
183 };
184
185 // Origin + 2 waypoints + destination.
186 expect(response.optimizedCount).toBe(4);
187 expect(response.order).toContain("stop-1");
188 expect(response.order).toContain("stop-2");
189 expect(response.distance).toBeGreaterThan(0);
190 expect(response.duration).toBeGreaterThan(0);
191 expect(typeof response.pricingBucket).toBe("string");
192 }),
193 { timeout: 120_000 },
194 );
195 });
196
197 describe("GeoRoutes.SnapToRoads", () => {
198 test.provider(
199 "snaps a GPS trace onto the road network",
200 (_stack) =>
201 Effect.gen(function* () {
202 const response = (yield* send(
203 HttpClientRequest.get(`${baseUrl}/snap-to-roads`),
204 ).pipe(Effect.flatMap((r) => r.json))) as {
205 snappedCount: number;
206 firstConfidence?: number;
207 pricingBucket?: string;
208 };
209
210 expect(response.snappedCount).toBeGreaterThan(0);
211 expect(response.firstConfidence).toBeGreaterThan(0);
212 expect(typeof response.pricingBucket).toBe("string");
213 }),
214 { timeout: 120_000 },
215 );
216 });
217 },
218);