t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Railway/Mongo.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

445 lines
14 KiB
TypeScript

import { RailwayAuth } from "@/Railway/AuthProvider.ts";
import { fromAuthProvider } from "@/Railway/Credentials.ts";
import { Query } from "@distilled.cloud/core/query";
import { GraphQLLive, Railway as RailwayApi } from "@distilled.cloud/railway";
import * as Alchemy from "@/index.ts";
import * as Provider from "@/Provider";
import * as Railway from "@/Railway";
import { suitePartition } from "./suiteProject.ts";
import { waitUntilVolumeGone } from "./waitUntilVolumeGone.ts";
import * as Test from "@/Test/Alchemy";
import { describe, expect } from "alchemy-test";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { MinimumLogLevel } from "effect/References";
import * as Schedule from "effect/Schedule";
import * as HttpClient from "effect/http/HttpClient";
import MongoApi, { Db, Site } from "./fixtures/mongo-api.ts";
const { test } = Test.make({
providers: Railway.providers(),
});
const logLevel = Effect.provideService(
MinimumLogLevel,
process.env.DEBUG ? "Debug" : "Info",
);
const distilled = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(
Effect.provide(
Layer.merge(
GraphQLLive,
fromAuthProvider().pipe(Layer.provide(RailwayAuth)),
),
),
);
const ping = (url: string) =>
Railway.pingMongo(url).pipe(
// Allow the new database and TCP proxy to become ready. Ten retries
// bound backoff to 50 seconds within the test's overall timeout.
Effect.retry({ schedule: Schedule.spaced("5 seconds"), times: 10 }),
);
const readVariables = Query.fn(
(projectId: string, environmentId: string, serviceId: string) =>
RailwayApi.variables({
projectId,
environmentId,
serviceId,
unrendered: true,
}),
);
const readServiceDeletedAt = Query.fn((id: string) => ({
deletedAt: RailwayApi.service({ id }).deletedAt,
}));
const readService = Query.fn((id: string) => {
const service = RailwayApi.service({ id });
return {
id: service.id,
name: service.name,
projectId: service.projectId,
deletedAt: service.deletedAt,
};
});
const readServiceInstance = Query.fn(
(environmentId: string, serviceId: string) => {
const instance = RailwayApi.serviceInstance({ environmentId, serviceId });
return {
serviceId: instance.serviceId,
image: instance.source.image,
startCommand: instance.startCommand,
};
},
);
const readVolumeInstance = Query.fn((id: string) => {
const volume = RailwayApi.volumeInstance({ id });
return {
id: volume.id,
volumeId: volume.volumeId,
mountPath: volume.mountPath,
serviceId: volume.serviceId,
};
});
const readTcpProxies = Query.fn((environmentId: string, serviceId: string) =>
RailwayApi.tcpProxies({ environmentId, serviceId }).pipe(
Query.map((proxy) => ({
id: proxy.id,
domain: proxy.domain,
proxyPort: proxy.proxyPort,
applicationPort: proxy.applicationPort,
deletedAt: proxy.deletedAt,
syncStatus: proxy.syncStatus,
})),
),
);
const asVariableMap = (value: unknown): Record<string, string> => {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
return {};
}
const out: Record<string, string> = {};
for (const [key, item] of Object.entries(value)) {
if (typeof item === "string") {
out[key] = item;
}
}
return out;
};
const readServiceVariables = (
projectId: string,
environmentId: string,
serviceId: string,
) =>
readVariables(projectId, environmentId, serviceId).pipe(
Effect.map(asVariableMap),
Effect.catchTag("RailwayNotFound", () =>
Effect.succeed({} as Record<string, string>),
),
);
const waitUntilServiceGone = (serviceId: string) =>
readServiceDeletedAt(serviceId).pipe(
Effect.map((service) =>
service.deletedAt != null ? ("gone" as const) : ("found" as const),
),
Effect.catchTag("RailwayNotFound", () => Effect.succeed("gone" as const)),
Effect.repeat({
schedule: Schedule.spaced("1 second"),
until: (status) => status === "gone",
times: 10,
}),
);
class NotReady extends Data.TaggedError("NotReady")<{
status: number;
body?: unknown;
}> {
override get message() {
return this.body === undefined
? `status ${this.status}`
: `status ${this.status}: ${JSON.stringify(this.body)}`;
}
}
const FixtureStack = Alchemy.Stack(
"RailwayMongoFixture",
{
providers: Railway.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const project = yield* Site;
const db = yield* Db;
const api = yield* MongoApi;
return {
projectId: project.projectId,
environmentId: db.environmentId,
serviceId: api.serviceId,
url: api.url,
publicConnectionUri: db.publicConnectionUri,
mode: "effect" as const,
};
}),
);
test.provider(
"create, ping, update, list, and delete mongo",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const created = yield* stack.deploy(
Effect.gen(function* () {
const { project, environment } = yield* suitePartition;
const db = yield* Railway.mongo("Db", { project, environment });
return { project, environment, db };
}),
);
expect(created.db.serviceId).toEqual(expect.any(String));
expect(created.db.serviceId.length).toBeGreaterThan(0);
expect(created.db.projectId).toEqual(created.project.projectId);
expect(created.db.environmentId).toEqual(
created.environment.environmentId,
);
expect(created.db.name).toEqual(expect.any(String));
expect(created.db.name.length).toBeGreaterThan(0);
expect(created.db.name.length).toBeLessThanOrEqual(32);
expect(created.db.name).toMatch(/^[a-z][a-z0-9-]*$/);
expect(created.db.user).toEqual("mongo");
expect(created.db.database).toEqual("railway");
expect(created.db.volumeId.length).toBeGreaterThan(0);
expect(created.db.volumeInstanceId.length).toBeGreaterThan(0);
expect(created.db.tcpProxyId).toEqual(expect.any(String));
expect(created.db.tcpProxyDomain).toEqual(expect.any(String));
expect(created.db.tcpProxyDomain).toContain("proxy.rlwy.net");
expect(created.db.tcpProxyPort).toEqual(expect.any(Number));
expect(created.db.tcpProxyPort).toBeGreaterThan(0);
expect(
created.db.connectionUri.includes(
`${created.db.name}.railway.internal`,
),
).toEqual(true);
expect(
created.db.publicConnectionUri.includes(created.db.tcpProxyDomain!),
).toEqual(true);
const fetched = yield* readService(created.db.serviceId);
expect(fetched.id).toEqual(created.db.serviceId);
expect(fetched.name).toEqual(created.db.name);
expect(fetched.projectId).toEqual(created.db.projectId);
expect(fetched.deletedAt).toBeNull();
const instance = yield* readServiceInstance(
created.db.environmentId,
created.db.serviceId,
);
expect(instance.serviceId).toEqual(created.db.serviceId);
expect(instance.image).toEqual(expect.stringContaining("mongo"));
expect(instance.startCommand ?? "").toContain("--ipv6");
expect(instance.startCommand ?? "").toContain("bind_ip");
const volume = yield* readVolumeInstance(created.db.volumeInstanceId);
expect(volume.id).toEqual(created.db.volumeInstanceId);
expect(volume.volumeId).toEqual(created.db.volumeId);
expect(volume.mountPath).toEqual("/data/db");
expect(volume.serviceId).toEqual(created.db.serviceId);
const proxies = yield* readTcpProxies(
created.db.environmentId,
created.db.serviceId,
);
const liveProxy = proxies.find(
(proxy) => proxy.deletedAt == null && proxy.syncStatus !== "DELETED",
);
expect(liveProxy).toBeDefined();
expect(liveProxy?.id).toEqual(created.db.tcpProxyId);
expect(liveProxy?.applicationPort).toEqual(27017);
const vars = yield* readServiceVariables(
created.db.projectId,
created.db.environmentId,
created.db.serviceId,
);
expect(vars.MONGO_INITDB_ROOT_USERNAME).toEqual("mongo");
expect((vars.MONGO_INITDB_ROOT_PASSWORD ?? "").length).toBeGreaterThan(0);
expect(vars[Railway.MONGO_URL_SECRET] ?? "").toContain(
"mongodb://${{MONGO_INITDB_ROOT_USERNAME}}",
);
expect(vars.MONGOHOST).toEqual("${{RAILWAY_PRIVATE_DOMAIN}}");
expect(vars.MONGOPORT).toEqual("27017");
const provider = yield* Provider.findProvider(Railway.Mongo);
const listed = yield* provider.list();
const found = listed.find(
(row) => row.serviceId === created.db.serviceId,
);
expect(found).toBeDefined();
expect(found?.name).toEqual(created.db.name);
expect(found?.projectId).toEqual(created.db.projectId);
const pong = yield* ping(created.db.publicConnectionUri);
expect(pong.ok).toEqual(1);
const nextName =
created.db.name.slice(0, -1) +
(created.db.name.endsWith("z") ? "y" : "z");
const updated = yield* stack.deploy(
Effect.gen(function* () {
const { project, environment } = yield* suitePartition;
const db = yield* Railway.mongo("Db", {
project,
environment,
name: nextName,
});
return { project, environment, db };
}),
);
expect(updated.db.serviceId).toEqual(created.db.serviceId);
expect(updated.db.name).toEqual(nextName);
expect(updated.db.projectId).toEqual(created.db.projectId);
expect(updated.db.volumeId).toEqual(created.db.volumeId);
expect(
updated.db.connectionUri.includes(`${nextName}.railway.internal`),
).toEqual(true);
const fetchedUpdate = yield* readService(updated.db.serviceId);
expect(fetchedUpdate.id).toEqual(updated.db.serviceId);
expect(fetchedUpdate.name).toEqual(nextName);
const pongUpdate = yield* ping(updated.db.publicConnectionUri);
expect(pongUpdate.ok).toEqual(1);
yield* stack.destroy();
const gone = yield* waitUntilServiceGone(created.db.serviceId);
expect(gone).toEqual("gone");
const volumeGone = yield* waitUntilVolumeGone(
created.db.volumeInstanceId,
);
expect(volumeGone).toEqual("gone");
}).pipe(logLevel),
{
tags: [
"provider:railway",
"provider:railway:mongo",
"provider:railway:project",
"provider:railway:projectenvironment",
"provider:railway:service",
"provider:railway:variable",
"live",
],
timeout: 120_000,
},
);
// Runtime fixture failures must not prevent the independent resource lifecycle test.
describe(
"ConnectMongo runtime integrations",
{
tags: [
"provider:railway",
"provider:railway:mongo",
"provider:railway:project",
"provider:railway:projectenvironment",
"provider:railway:service",
"provider:railway:variable",
"live",
],
},
() => {
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
providers: Railway.providers(),
});
const fixture = beforeAll(deploy(FixtureStack), {
timeout: 120_000,
});
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(FixtureStack), {
timeout: 120_000,
});
test(
"a Service connects and pings through ConnectMongo",
Effect.gen(function* () {
const out = yield* fixture;
expect(out.serviceId).toEqual(expect.any(String));
expect(out.serviceId.length).toBeGreaterThan(0);
expect(out.url).toEqual(expect.any(String));
expect(out.url).toContain("up.railway.app");
const fetched = yield* distilled(readService(out.serviceId));
expect(fetched.id).toEqual(out.serviceId);
expect(fetched.deletedAt).toBeNull();
const vars = yield* distilled(
readServiceVariables(out.projectId, out.environmentId, out.serviceId),
);
expect((vars[Railway.MONGO_URL_SECRET] ?? "").length).toBeGreaterThan(
0,
);
const client = yield* HttpClient.HttpClient;
const get = (path: string) =>
client.get(`${out.url}${path}`).pipe(
Effect.timeoutOrElse({
duration: "8 seconds",
orElse: () => Effect.fail(new NotReady({ status: 0 })),
}),
Effect.flatMap((res) =>
res.status === 200
? res.json.pipe(
Effect.mapError(() => new NotReady({ status: res.status })),
)
: Effect.fail(new NotReady({ status: res.status })),
),
Effect.retry({
while: (e) =>
e._tag === "NotReady" &&
(e.status === 0 ||
e.status === 404 ||
e.status === 502 ||
e.status === 503),
schedule: Schedule.exponential("500 millis").pipe(
Schedule.upTo({ duration: "45 seconds" }),
),
times: 10,
}),
);
const getText = client.get(out.url!).pipe(
Effect.timeoutOrElse({
duration: "8 seconds",
orElse: () => Effect.fail(new NotReady({ status: 0 })),
}),
Effect.flatMap((res) =>
res.status === 200
? res.text.pipe(
Effect.mapError(() => new NotReady({ status: res.status })),
)
: Effect.fail(new NotReady({ status: res.status })),
),
Effect.retry({
while: (e) =>
e._tag === "NotReady" &&
(e.status === 0 ||
e.status === 404 ||
e.status === 502 ||
e.status === 503),
schedule: Schedule.exponential("500 millis").pipe(
Schedule.upTo({ duration: "45 seconds" }),
),
times: 10,
}),
);
if (out.mode === "effect") {
const pingBody = (yield* get("/ping")) as { ok?: boolean };
expect(pingBody.ok).toEqual(true);
const health = (yield* get("/health")) as { ok?: number };
expect(health.ok).toEqual(1);
} else {
const body = yield* getText;
expect(typeof body).toEqual("string");
expect(body.length).toBeGreaterThan(0);
}
const pong = yield* ping(out.publicConnectionUri);
expect(pong.ok).toEqual(1);
}).pipe(logLevel),
{ timeout: 120_000 },
);
},
);