| 1 | import { RuntimeContext } from "alchemy"; |
| 2 | import { describe, expect, it } from "alchemy-test"; |
| 3 | import * as BunServices from "@effect/platform-bun/BunServices"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Path from "effect/Path"; |
| 7 | import { BetterAuth, Database } from "@/index.ts"; |
| 8 | import { Drizzle } from "@/Drizzle.ts"; |
| 9 | import * as schema from "./fixtures/drizzle-auth-schema.ts"; |
| 10 | |
| 11 | describe("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 | }); |