1import { RuntimeContext } from "alchemy";
2import { describe, expect, it } from "alchemy-test";
3import * as BunServices from "@effect/platform-bun/BunServices";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import { BetterAuth, Database } from "@/index.ts";
8import { Drizzle } from "@/Drizzle.ts";
9import * as schema from "./fixtures/drizzle-auth-schema.ts";
10
11describe("BetterAuth (drizzle)", { tags: ["unit", "local"] }, () => {
12 it.live(
13 "uses generated Relations v2 for sign-up, sign-in, and joined sessions",
14 () =>
15 Effect.gen(function* () {
16 const fs = yield* FileSystem.FileSystem;
17 const path = yield* Path.Path;
18 const migration = yield* fs.readFileString(
19 yield* path.fromFileUrl(
20 new URL("./fixtures/drizzle-auth.sql", import.meta.url),
21 ),
22 );
23 const { drizzle } = yield* Effect.promise(
24 () => import("drizzle-orm/bun-sqlite"),
25 );
26 const { Database: BunSqlite } = yield* Effect.promise(
27 () => import("bun:sqlite"),
28 );
29 const client = yield* Effect.acquireRelease(
30 Effect.sync(() => new BunSqlite(":memory:")),
31 (client) => Effect.sync(() => client.close()),
32 );
33 yield* Effect.sync(() => client.exec(migration));
34 const db = yield* Effect.sync(() =>
35 drizzle({ client, relations: schema.authRelations }),
36 );
37 const auth = yield* BetterAuth({
38 baseURL: "http://localhost:3000",
39 secret: "test-secret-test-secret-test-secret",
40 emailAndPassword: { enabled: true },
41 advanced: { database: { joins: true } },
42 }).pipe(Effect.provide(Drizzle(db, { provider: "sqlite", schema })));
43 const signUp = yield* auth.api.signUpEmail({
44 body: {
45 email: "drizzle@example.com",
46 password: "password1234",
47 name: "Drizzle User",
48 },
49 });
50 const raw = yield* auth.auth;
51 const response = yield* Effect.promise(() =>
52 raw.api.signInEmail({
53 body: { email: "drizzle@example.com", password: "password1234" },
54 asResponse: true,
55 }),
56 );
57 expect(response.status).toBe(200);
58 const cookie = response.headers
59 .getSetCookie()
60 .map((value) => value.split(";")[0])
61 .join("; ");
62 const session = yield* auth.getSession(new Headers({ cookie }));
63 expect(session?.user.id).toBe(signUp.user.id);
64 expect(session?.user.email).toBe("drizzle@example.com");
65 expect(yield* auth.getSession(new Headers())).toBeNull();
66 }).pipe(
67 Effect.scoped,
68 Effect.provide(BunServices.layer),
69 Effect.provide(RuntimeContext.phantom),
70 ),
71 );
72
73 it.live("accepts a request-scoped Postgres client", () =>
74 Effect.gen(function* () {
75 const { drizzle } = yield* Effect.promise(
76 () => import("drizzle-orm/node-postgres"),
77 );
78 const { Pool } = yield* Effect.promise(() => import("pg"));
79 let acquired = 0;
80 let released = 0;
81 const database = Effect.gen(function* () {
82 const client = yield* Effect.acquireRelease(
83 Effect.sync(() => {
84 acquired++;
85 return new Pool();
86 }),
87 (pool) =>
88 Effect.promise(() => pool.end()).pipe(
89 Effect.tap(() => Effect.sync(() => released++)),
90 ),
91 );
92 return yield* Effect.sync(() => drizzle({ client }));
93 });
94 const service = yield* Database.pipe(
95 Effect.provide(Drizzle(database, { provider: "pg" })),
96 );
97 expect(acquired).toBe(0);
98 expect(typeof (yield* service.runtime.pipe(Effect.scoped))).toBe(
99 "function",
100 );
101 expect(acquired).toBe(1);
102 expect(released).toBe(1);
103 }).pipe(Effect.provide(RuntimeContext.phantom)),
104 );
105
106 it.live("wraps an existing drizzle db via the official adapter", () =>
107 Effect.gen(function* () {
108 const { drizzle } = yield* Effect.promise(
109 () => import("drizzle-orm/bun-sqlite"),
110 );
111 const { Database: BunSqlite } = yield* Effect.promise(
112 () => import("bun:sqlite"),
113 );
114 const db = drizzle({ client: new BunSqlite(":memory:") });
115
116 const service = yield* Database.pipe(
117 Effect.provide(
118 Drizzle(db, {
119 provider: "sqlite",
120 }),
121 ),
122 );
123 // "pg" | "mysql" | "sqlite" maps onto the Database provider kinds
124 expect(service.provider).toBe("sqlite");
125 // no automatic migration support — schema is user-owned
126 expect(service.migrate).toBeUndefined();
127 // the runtime input is better-auth's adapter factory
128 const input = yield* Effect.scoped(service.runtime);
129 expect(typeof input).toBe("function");
130 }).pipe(Effect.provide(RuntimeContext.phantom)),
131 );
132});