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 GeoPlacesTestFunctionLive, { GeoPlacesTestFunction } from "./handler.ts";
11
12const testOptions = { providers: AWS.providers() };
13const { test, beforeAll, afterAll } = Test.make(testOptions);
14const sharedStack = Core.scratchStack(testOptions, "GeoPlacesBindings");
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 "GeoPlaces Bindings",
52 {
53 tags: [
54 "provider:aws",
55 "provider:aws:geoplaces",
56 "provider:aws:lambda",
57 "live",
58 ],
59 },
60 () => {
61 beforeAll(
62 Effect.gen(function* () {
63 yield* Effect.logInfo(
64 "GeoPlaces test setup: destroying previous resources",
65 );
66 yield* sharedStack.destroy();
67
68 yield* Effect.logInfo("GeoPlaces test setup: deploying fixture");
69 const { functionUrl } = yield* sharedStack.deploy(
70 Effect.gen(function* () {
71 return yield* GeoPlacesTestFunction;
72 }).pipe(Effect.provide(GeoPlacesTestFunctionLive)),
73 );
74
75 expect(functionUrl).toBeTruthy();
76 baseUrl = functionUrl!.replace(/\/+$/, "");
77 const readinessUrl = `${baseUrl}/ping`;
78
79 yield* Effect.logInfo(
80 `GeoPlaces 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 `GeoPlaces 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("GeoPlaces.Autocomplete", () => {
104 test.provider(
105 "completes a partial address query",
106 (_stack) =>
107 Effect.gen(function* () {
108 const response = (yield* send(
109 HttpClientRequest.get(`${baseUrl}/autocomplete`),
110 ).pipe(Effect.flatMap((r) => r.json))) as {
111 count: number;
112 firstTitle?: string;
113 };
114
115 expect(response.count).toBeGreaterThan(0);
116 expect(typeof response.firstTitle).toBe("string");
117 expect(response.firstTitle!.length).toBeGreaterThan(0);
118 }),
119 { timeout: 120_000 },
120 );
121 });
122
123 describe("GeoPlaces.Geocode", () => {
124 test.provider(
125 "geocodes an address into coordinates",
126 (_stack) =>
127 Effect.gen(function* () {
128 const response = (yield* send(
129 HttpClientRequest.get(`${baseUrl}/geocode`),
130 ).pipe(Effect.flatMap((r) => r.json))) as {
131 count: number;
132 position?: number[];
133 };
134
135 expect(response.count).toBeGreaterThan(0);
136 expect(Array.isArray(response.position)).toBe(true);
137 expect(response.position).toHaveLength(2);
138 }),
139 { timeout: 120_000 },
140 );
141 });
142
143 describe("GeoPlaces.GetPlace", () => {
144 test.provider(
145 "fetches place details by PlaceId",
146 (_stack) =>
147 Effect.gen(function* () {
148 const response = (yield* send(
149 HttpClientRequest.get(`${baseUrl}/get-place`),
150 ).pipe(Effect.flatMap((r) => r.json))) as {
151 placeId?: string;
152 label?: string;
153 pricingBucket?: string;
154 };
155
156 expect(typeof response.placeId).toBe("string");
157 expect(response.placeId!.length).toBeGreaterThan(0);
158 expect(typeof response.label).toBe("string");
159 expect(response.label!.length).toBeGreaterThan(0);
160 expect(typeof response.pricingBucket).toBe("string");
161 }),
162 { timeout: 120_000 },
163 );
164 });
165
166 describe("GeoPlaces.ReverseGeocode", () => {
167 test.provider(
168 "reverse geocodes coordinates into an address",
169 (_stack) =>
170 Effect.gen(function* () {
171 const response = (yield* send(
172 HttpClientRequest.get(`${baseUrl}/reverse-geocode`),
173 ).pipe(Effect.flatMap((r) => r.json))) as {
174 count: number;
175 label?: string;
176 };
177
178 expect(response.count).toBeGreaterThan(0);
179 expect(typeof response.label).toBe("string");
180 expect(response.label!.length).toBeGreaterThan(0);
181 }),
182 { timeout: 120_000 },
183 );
184 });
185
186 describe("GeoPlaces.SearchNearby", () => {
187 test.provider(
188 "finds places around a position",
189 (_stack) =>
190 Effect.gen(function* () {
191 const response = (yield* send(
192 HttpClientRequest.get(`${baseUrl}/search-nearby`),
193 ).pipe(Effect.flatMap((r) => r.json))) as {
194 count: number;
195 firstTitle?: string;
196 pricingBucket?: string;
197 };
198
199 expect(response.count).toBeGreaterThan(0);
200 expect(typeof response.firstTitle).toBe("string");
201 expect(response.firstTitle!.length).toBeGreaterThan(0);
202 expect(typeof response.pricingBucket).toBe("string");
203 }),
204 { timeout: 120_000 },
205 );
206 });
207
208 describe("GeoPlaces.SearchText", () => {
209 test.provider(
210 "returns ranked results for a text query",
211 (_stack) =>
212 Effect.gen(function* () {
213 const response = (yield* send(
214 HttpClientRequest.get(`${baseUrl}/search-text`),
215 ).pipe(Effect.flatMap((r) => r.json))) as {
216 count: number;
217 firstPosition?: number[];
218 };
219
220 expect(response.count).toBeGreaterThan(0);
221 expect(Array.isArray(response.firstPosition)).toBe(true);
222 expect(response.firstPosition).toHaveLength(2);
223 }),
224 { timeout: 120_000 },
225 );
226 });
227
228 describe("GeoPlaces.Suggest", () => {
229 test.provider(
230 "suggests places for a free-form query",
231 (_stack) =>
232 Effect.gen(function* () {
233 const response = (yield* send(
234 HttpClientRequest.get(`${baseUrl}/suggest`),
235 ).pipe(Effect.flatMap((r) => r.json))) as {
236 count: number;
237 firstTitle?: string;
238 firstType?: string;
239 };
240
241 expect(response.count).toBeGreaterThan(0);
242 expect(typeof response.firstTitle).toBe("string");
243 expect(response.firstTitle!.length).toBeGreaterThan(0);
244 expect(typeof response.firstType).toBe("string");
245 }),
246 { timeout: 120_000 },
247 );
248 });
249 },
250);