Skip to content
Source
Sign inSign up
Open History to see the last commit

FunctionEventSource.test.ts

140 lines4.4 KB

1import { providers } from "@alchemy.run/neon/Providers";
2import { expect } from "@alchemy.run/test";
3import * as Test from "@alchemy.run/test/Alchemy";
4import * as Api from "@distilled.cloud/neon";
5import * as Alchemy from "alchemy";
6import * as Clock from "effect/Clock";
7import * as Duration from "effect/Duration";
8import * as Effect from "effect/Effect";
9import * as HttpClient from "effect/http/HttpClient";
10import * as Schedule from "effect/Schedule";
11import EventFunction from "./fixtures/function-events.ts";
12​
13const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
14 providers: providers(),
15});
16const Stack = Alchemy.Stack(
17 "NeonFunctionEvents",
18 { providers: providers(), state: Alchemy.localState() },
19 Effect.gen(function* () {
20 const api = yield* EventFunction;
21 return {
22 url: api.url,
23 projectId: api.projectId,
24 branchId: api.branchId,
25 slug: api.slug,
26 };
27 }),
28);
29const stack = beforeAll(destroy(Stack).pipe(Effect.andThen(deploy(Stack))));
30afterAll(destroy(Stack));
31​
32test.provider(
33 "event-source bindings register independent triggers without dependency cycles",
34 () =>
35 Effect.gen(function* () {
36 const deployed = yield* stack;
37 const { triggers } = yield* Api.listProjectBranchTriggers({
38 project_id: deployed.projectId,
39 branch_id: deployed.branchId,
40 });
41 expect(
42 triggers
43 .filter((trigger) => trigger.function_slug === deployed.slug)
44 .map((trigger) => trigger.type)
45 .sort(),
46 ).toEqual(["schedule", "storage_object_created"]);
47 expect(triggers.every((trigger) => trigger.enabled)).toBe(true);
48 }),
49 {
50 tags: [
51 "provider:neon",
52 "provider:neon:bucket",
53 "provider:neon:function",
54 "provider:neon:project",
55 "live",
56 ],
57 timeout: 120_000,
58 },
59);
60​
61test(
62 "real bucket uploads invoke the typed prefix-filtered event route",
63 Effect.gen(function* () {
64 const { url } = yield* stack;
65 const client = yield* HttpClient.HttpClient;
66 expect((yield* client.post(`${url}upload`)).status).toBe(204);
67 // Bucket event delivery is eventually consistent; poll up to 2 minutes.
68 const events = yield* client.get(url).pipe(
69 Effect.flatMap((response) => response.json),
70 Effect.repeat({
71 schedule: Schedule.spaced("5 seconds"),
72 until: (body) => JSON.stringify(body).includes("incoming/test.txt"),
73 }),
74 Effect.timeout("2 minutes"),
75 );
76 expect(events).toEqual(
77 expect.arrayContaining([
78 expect.objectContaining({
79 kind: "upload",
80 object_key: "incoming/test.txt",
81 }),
82 ]),
83 );
84 expect(JSON.stringify(events)).not.toContain("outside.txt");
85 }),
86 {
87 tags: [
88 "provider:neon",
89 "provider:neon:bucket",
90 "provider:neon:function",
91 "provider:neon:project",
92 "live",
93 ],
94 timeout: 180_000,
95 },
96);
97​
98// Neon skipped the first occurrence after the trigger was created in a live
99// probe and delivered the next one within ~4s, so wait through the occurrence
100// after the reported next_run_at (up to ~2 minutes); skip under --fast.
101test.provider.skipIf(!!process.env.FAST)(
102 "real minute schedule invokes the typed cron route",
103 () =>
104 Effect.gen(function* () {
105 const { url, projectId, branchId, slug } = yield* stack;
106 const { triggers } = yield* Api.listProjectBranchTriggers({
107 project_id: projectId,
108 branch_id: branchId,
109 });
110 const minute = triggers.find(
111 (trigger): trigger is Api.ScheduleTrigger =>
112 trigger.type === "schedule" && trigger.function_slug === slug,
113 );
114 expect(minute?.next_run_at).toEqual(expect.any(String));
115 const nextRunAt = Date.parse(minute!.next_run_at!);
116 const now = yield* Clock.currentTimeMillis;
117 const client = yield* HttpClient.HttpClient;
118 const events = yield* client.get(url).pipe(
119 Effect.flatMap((response) => response.json),
120 Effect.repeat({
121 schedule: Schedule.spaced("5 seconds"),
122 until: (body) => JSON.stringify(body).includes('"schedule"'),
123 }),
124 Effect.timeout(Duration.millis(Math.max(nextRunAt - now, 0) + 75_000)),
125 );
126 expect(events).toEqual(
127 expect.arrayContaining([expect.objectContaining({ kind: "schedule" })]),
128 );
129 }),
130 {
131 tags: [
132 "provider:neon",
133 "provider:neon:bucket",
134 "provider:neon:function",
135 "provider:neon:project",
136 "live",
137 ],
138 timeout: 180_000,
139 },
140);