1import { Project } from "@/Neon/Project";
2import { Branch } from "@/Neon/Branch";
3import { Function } from "@/Neon/Function";
4import { FunctionTrigger } from "@/Neon/FunctionTrigger";
5import { providers } from "@/Neon/Providers";
6import * as Test from "@/Test/Alchemy";
7import * as Api from "@distilled.cloud/neon";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import * as HttpClient from "effect/http/HttpClient";
11import * as HttpClientRequest from "effect/http/HttpClientRequest";
12
13const { test } = Test.make({ providers: providers() });
14test.provider(
15 "schedule lifecycle, inherited disabled state and spoofed attestation",
16 (stack) =>
17 Effect.gen(function* () {
18 yield* stack.destroy();
19 const deploy = (cron: string, enabled: boolean, child = false) =>
20 stack.deploy(
21 Effect.gen(function* () {
22 const project = yield* Project("TriggerProject", {
23 region: "aws-us-east-2",
24 });
25 const api = yield* Function("Api", {
26 project,
27 main: new URL("./fixtures/function-trigger.ts", import.meta.url)
28 .href,
29 });
30 const trigger = yield* FunctionTrigger("Schedule", {
31 function: api,
32 type: "schedule",
33 schedule: { cron },
34 path: "/jobs",
35 enabled,
36 });
37 const preview = child
38 ? yield* Branch("Preview", { project })
39 : undefined;
40 return { project, api, trigger, preview };
41 }),
42 );
43 const first = yield* deploy("0 2 * * *", false);
44 expect(first.trigger.enabled).toBe(false);
45 const changed = yield* deploy("0 3 * * *", true, true);
46 expect(changed.trigger.triggerId).toBe(first.trigger.triggerId);
47 expect(changed.trigger.version).toBeGreaterThan(first.trigger.version);
48 const observed = yield* Api.getProjectBranchTrigger({
49 project_id: changed.trigger.projectId,
50 branch_id: changed.trigger.branchId,
51 trigger_id: changed.trigger.triggerId,
52 });
53 expect(observed.trigger).toMatchObject({
54 enabled: true,
55 type: "schedule",
56 schedule: { cron: "0 3 * * *" },
57 });
58 const inherited = yield* Api.getProjectBranchTrigger({
59 project_id: changed.project.projectId,
60 branch_id: changed.preview!.branchId,
61 trigger_id: changed.trigger.triggerId,
62 });
63 expect(inherited.trigger).toMatchObject({
64 inherited: true,
65 enabled: false,
66 next_run_at: null,
67 });
68 const client = yield* HttpClient.HttpClient;
69 const spoof = yield* client.execute(
70 HttpClientRequest.post(`${changed.api.url}jobs`).pipe(
71 HttpClientRequest.setHeader("x-neon-trigger-invocation-id", "spoof"),
72 HttpClientRequest.bodyJsonUnsafe({
73 version: 1,
74 invocation_id: "spoof",
75 trigger: { type: "schedule" },
76 data: {},
77 }),
78 ),
79 );
80 expect(spoof.status).toBe(403);
81 yield* stack.deploy(
82 Effect.gen(function* () {
83 const project = yield* Project("TriggerProject", {
84 region: "aws-us-east-2",
85 });
86 yield* Function("Api", {
87 project,
88 main: new URL("./fixtures/function-trigger.ts", import.meta.url)
89 .href,
90 });
91 yield* Branch("Preview", { project });
92 }),
93 );
94 expect(
95 yield* Api.getProjectBranchTrigger({
96 project_id: changed.trigger.projectId,
97 branch_id: changed.trigger.branchId,
98 trigger_id: changed.trigger.triggerId,
99 }).pipe(
100 Effect.as(false),
101 Effect.catchTag("NotFound", () => Effect.succeed(true)),
102 ),
103 ).toBe(true);
104 yield* stack.destroy();
105 }),
106 {
107 tags: [
108 "provider:neon",
109 "provider:neon:branch",
110 "provider:neon:function",
111 "provider:neon:project",
112 "live",
113 ],
114 timeout: 120_000,
115 },
116);