1import { Query } from "@distilled.cloud/core/query";
2import { Railway as RailwayApi } from "@distilled.cloud/railway";
3import * as Railway from "@/Railway";
4import * as Test from "@/Test/Alchemy";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import { suitePartition } from "../suiteProject.ts";
10
11const { test } = Test.make({ providers: Railway.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const origin = Effect.gen(function* () {
19 const { project, environment } = yield* suitePartition;
20 // A public domain must exist before Railway can enable CDN caching.
21 const service = yield* Railway.Service("Origin", {
22 project,
23 environment,
24 image: "nginx:alpine",
25 port: 80,
26 healthcheckPath: "/",
27 });
28 return { project, environment, service };
29});
30
31const readConfig = Query.fn((serviceId: string, environmentId: string) => ({
32 edgeConfig: RailwayApi.serviceInstance({
33 serviceId,
34 environmentId,
35 }).edgeConfig.pipe(
36 Query.map((config) => ({
37 id: config.id,
38 enabled: config.enabled,
39 caching: config.caching.pipe(
40 Query.map((caching) => ({
41 mode: caching.mode,
42 htmlCaching: caching.htmlCaching,
43 purgeOnDeploy: caching.purgeOnDeploy,
44 defaultTtlSeconds: caching.defaultTtlSeconds,
45 })),
46 ),
47 })),
48 ),
49}));
50
51const readCachingState = Query.fn(
52 (serviceId: string, environmentId: string) => {
53 const instance = RailwayApi.serviceInstance({ serviceId, environmentId });
54 return {
55 serviceId: instance.serviceId,
56 edgeConfig: instance.edgeConfig.pipe(
57 Query.map((config) => ({
58 enabled: config.enabled,
59 caching: config.caching.pipe(
60 Query.map((caching) => ({ mode: caching.mode })),
61 ),
62 })),
63 ),
64 };
65 },
66);
67
68test.provider(
69 "enable, update, and disable CDN while retaining its origin",
70 (stack) =>
71 Effect.gen(function* () {
72 yield* stack.destroy();
73
74 const created = yield* stack.deploy(
75 Effect.gen(function* () {
76 const resources = yield* origin;
77 const cdn = yield* Railway.Website.Cdn("Cache", {
78 service: resources.service,
79 environment: resources.environment,
80 htmlCaching: "AUTO",
81 purgeOnDeploy: "HTML",
82 defaultTtlSeconds: 60,
83 });
84 return { ...resources, cdn };
85 }),
86 );
87
88 expect(created.service.domain).toEqual(expect.any(String));
89 expect(created.cdn.enabled).toEqual(true);
90 const initial = yield* readConfig(
91 created.service.serviceId,
92 created.environment.environmentId,
93 );
94 expect(initial.edgeConfig?.id).toEqual(created.cdn.edgeConfigId);
95 expect(initial.edgeConfig?.enabled).toEqual(true);
96 expect(initial.edgeConfig?.caching?.mode.toLowerCase()).not.toEqual(
97 "off",
98 );
99 expect(initial.edgeConfig?.caching?.htmlCaching).toEqual("auto");
100 expect(initial.edgeConfig?.caching?.purgeOnDeploy).toEqual("HTML");
101 expect(initial.edgeConfig?.caching?.defaultTtlSeconds).toEqual(60);
102
103 const updated = yield* stack.deploy(
104 Effect.gen(function* () {
105 const resources = yield* origin;
106 const cdn = yield* Railway.Website.Cdn("Cache", {
107 service: resources.service,
108 environment: resources.environment,
109 htmlCaching: "FORCE",
110 purgeOnDeploy: "ALL",
111 defaultTtlSeconds: 120,
112 });
113 return { ...resources, cdn };
114 }),
115 );
116
117 expect(updated.service.serviceId).toEqual(created.service.serviceId);
118 expect(updated.cdn.edgeConfigId).toEqual(created.cdn.edgeConfigId);
119 expect(updated.cdn.enabled).toEqual(true);
120 const changed = yield* readConfig(
121 updated.service.serviceId,
122 updated.environment.environmentId,
123 );
124 expect(changed.edgeConfig?.enabled).toEqual(true);
125 expect(changed.edgeConfig?.caching?.mode.toLowerCase()).not.toEqual(
126 "off",
127 );
128 expect(changed.edgeConfig?.caching?.htmlCaching).toEqual("force");
129 expect(changed.edgeConfig?.caching?.purgeOnDeploy).toEqual("ALL");
130 expect(changed.edgeConfig?.caching?.defaultTtlSeconds).toEqual(120);
131
132 // Keep both dependencies deployed so this step tests CDN deletion alone.
133 const retained = yield* stack.deploy(origin);
134 expect(retained.service.serviceId).toEqual(created.service.serviceId);
135 expect(retained.environment.environmentId).toEqual(
136 created.environment.environmentId,
137 );
138
139 const disabled = yield* readCachingState(
140 retained.service.serviceId,
141 retained.environment.environmentId,
142 ).pipe(
143 Effect.map((instance) => ({
144 serviceId: instance.serviceId,
145 // Edge routing can stay enabled after CDN caching is switched off.
146 cachingEnabled:
147 instance.edgeConfig?.enabled === true &&
148 instance.edgeConfig.caching != null &&
149 instance.edgeConfig.caching.mode.toLowerCase() !== "off",
150 })),
151 Effect.repeat({
152 schedule: Schedule.spaced("1 second"),
153 until: (observed) => !observed.cachingEnabled,
154 times: 10,
155 }),
156 );
157 expect(disabled.serviceId).toEqual(retained.service.serviceId);
158 expect(disabled.cachingEnabled).toEqual(false);
159
160 yield* stack.destroy();
161 }).pipe(logLevel),
162 {
163 tags: [
164 "provider:railway",
165 "provider:railway:project",
166 "provider:railway:projectenvironment",
167 "provider:railway:service",
168 "provider:railway:website",
169 "live",
170 ],
171 timeout: 120_000,
172 },
173);