1import * as Alchemy from "@/index";
2import { providers } from "@/Neon/Providers";
3import * as Test from "@/Test/Alchemy";
4import * as Api from "@distilled.cloud/neon";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import EventFunction from "./fixtures/function-events.ts";
10
11const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
12 providers: providers(),
13});
14const Stack = Alchemy.Stack(
15 "NeonFunctionEvents",
16 { providers: providers(), state: Alchemy.localState() },
17 Effect.gen(function* () {
18 const api = yield* EventFunction;
19 return {
20 url: api.url,
21 projectId: api.projectId,
22 branchId: api.branchId,
23 slug: api.slug,
24 };
25 }),
26);
27const stack = beforeAll(destroy(Stack).pipe(Effect.andThen(deploy(Stack))));
28afterAll(destroy(Stack));
29
30test.provider(
31 "event-source bindings register independent triggers without dependency cycles",
32 () =>
33 Effect.gen(function* () {
34 const deployed = yield* stack;
35 const { triggers } = yield* Api.listProjectBranchTriggers({
36 project_id: deployed.projectId,
37 branch_id: deployed.branchId,
38 });
39 expect(
40 triggers
41 .filter((trigger) => trigger.function_slug === deployed.slug)
42 .map((trigger) => trigger.type)
43 .sort(),
44 ).toEqual(["schedule", "storage_object_created"]);
45 expect(triggers.every((trigger) => trigger.enabled)).toBe(true);
46 }),
47 {
48 tags: [
49 "provider:neon",
50 "provider:neon:bucket",
51 "provider:neon:function",
52 "provider:neon:project",
53 "live",
54 ],
55 timeout: 120_000,
56 },
57);
58
59test(
60 "real bucket uploads invoke the typed prefix-filtered event route",
61 Effect.gen(function* () {
62 const { url } = yield* stack;
63 const client = yield* HttpClient.HttpClient;
64 expect((yield* client.post(`${url}upload`)).status).toBe(204);
65 // Bucket event delivery is eventually consistent; poll up to 2 minutes.
66 const events = yield* client.get(url).pipe(
67 Effect.flatMap((response) => response.json),
68 Effect.repeat({
69 schedule: Schedule.spaced("5 seconds"),
70 until: (body) => JSON.stringify(body).includes("incoming/test.txt"),
71 }),
72 Effect.timeout("2 minutes"),
73 );
74 expect(events).toEqual(
75 expect.arrayContaining([
76 expect.objectContaining({
77 kind: "upload",
78 object_key: "incoming/test.txt",
79 }),
80 ]),
81 );
82 expect(JSON.stringify(events)).not.toContain("outside.txt");
83 }),
84 {
85 tags: [
86 "provider:neon",
87 "provider:neon:bucket",
88 "provider:neon:function",
89 "provider:neon:project",
90 "live",
91 ],
92 timeout: 180_000,
93 },
94);
95
96test(
97 "real minute schedule invokes the typed cron route",
98 Effect.gen(function* () {
99 const { url } = yield* stack;
100 const client = yield* HttpClient.HttpClient;
101 const events = yield* client.get(url).pipe(
102 Effect.flatMap((response) => response.json),
103 Effect.repeat({
104 schedule: Schedule.spaced("6 seconds"),
105 times: 9,
106 until: (body) => JSON.stringify(body).includes('"schedule"'),
107 }),
108 );
109 expect(events).toEqual(
110 expect.arrayContaining([expect.objectContaining({ kind: "schedule" })]),
111 );
112 }),
113 {
114 tags: [
115 "provider:neon",
116 "provider:neon:bucket",
117 "provider:neon:function",
118 "provider:neon:project",
119 "live",
120 ],
121 timeout: 120_000,
122 },
123);