diff --git a/.gitignore b/.gitignore index 7949ab3..fe86910 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* # SQLite Stuff -database.db +database.db* # Devenv .devenv* diff --git a/bun.lockb b/bun.lockb index 640ee61..e198b53 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/drizzle/0000_open_famine.sql b/drizzle/0000_hot_sage.sql similarity index 60% rename from drizzle/0000_open_famine.sql rename to drizzle/0000_hot_sage.sql index 9d9cbf0..3e86f36 100644 --- a/drizzle/0000_open_famine.sql +++ b/drizzle/0000_hot_sage.sql @@ -1,11 +1,23 @@ CREATE TABLE `email_addresses` ( `email_id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `user_id` text(26), + `user_id` text(26) NOT NULL, `email_address` text NOT NULL, `is_verified` integer DEFAULT 0 NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint +CREATE TABLE `passwords` ( + `user_id` text PRIMARY KEY NOT NULL, + `password_hash` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `public_assets` ( + `file_name` text PRIMARY KEY NOT NULL, + `type` text NOT NULL, + `blob` blob NOT NULL +); +--> statement-breakpoint CREATE TABLE `user_aliases` ( `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `user_ref` text NOT NULL, @@ -16,7 +28,9 @@ CREATE TABLE `user_aliases` ( CREATE TABLE `users` ( `id` text PRIMARY KEY NOT NULL, `display_name` text, - `username` text NOT NULL + `username` text NOT NULL, + `profile_picture` text, + FOREIGN KEY (`profile_picture`) REFERENCES `public_assets`(`file_name`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint CREATE UNIQUE INDEX `email_addresses_email_address_unique` ON `email_addresses` (`email_address`);--> statement-breakpoint diff --git a/drizzle/0001_spicy_thena.sql b/drizzle/0001_spicy_thena.sql deleted file mode 100644 index f80d2b2..0000000 --- a/drizzle/0001_spicy_thena.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE `public_assets` ( - `file_name` text PRIMARY KEY NOT NULL, - `type` text NOT NULL, - `blob` blob NOT NULL -); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index e1d21f1..a9305b6 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "f3c55a7a-8bd7-451f-b5c2-ad2370a0829c", + "id": "86be13c9-c3e4-4d58-b449-58f12e9dfda3", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "email_addresses": { @@ -18,7 +18,7 @@ "name": "user_id", "type": "text(26)", "primaryKey": false, - "notNull": false, + "notNull": true, "autoincrement": false }, "email_address": { @@ -58,6 +58,69 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, + "passwords": { + "name": "passwords", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "passwords_user_id_users_id_fk": { + "name": "passwords_user_id_users_id_fk", + "tableFrom": "passwords", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public_assets": { + "name": "public_assets", + "columns": { + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob": { + "name": "blob", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, "user_aliases": { "name": "user_aliases", "columns": { @@ -121,6 +184,13 @@ "primaryKey": false, "notNull": true, "autoincrement": false + }, + "profile_picture": { + "name": "profile_picture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false } }, "indexes": { @@ -130,7 +200,17 @@ "isUnique": true } }, - "foreignKeys": {}, + "foreignKeys": { + "users_profile_picture_public_assets_file_name_fk": { + "name": "users_profile_picture_public_assets_file_name_fk", + "tableFrom": "users", + "tableTo": "public_assets", + "columnsFrom": ["profile_picture"], + "columnsTo": ["file_name"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": {} } diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json deleted file mode 100644 index 6eda929..0000000 --- a/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "d18f7878-d194-4bdd-971d-79e9645dc74c", - "prevId": "f3c55a7a-8bd7-451f-b5c2-ad2370a0829c", - "tables": { - "email_addresses": { - "name": "email_addresses", - "columns": { - "email_id": { - "name": "email_id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "user_id": { - "name": "user_id", - "type": "text(26)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email_address": { - "name": "email_address", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_verified": { - "name": "is_verified", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "0" - } - }, - "indexes": { - "email_addresses_email_address_unique": { - "name": "email_addresses_email_address_unique", - "columns": ["email_address"], - "isUnique": true - } - }, - "foreignKeys": { - "email_addresses_user_id_users_id_fk": { - "name": "email_addresses_user_id_users_id_fk", - "tableFrom": "email_addresses", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public_assets": { - "name": "public_assets", - "columns": { - "file_name": { - "name": "file_name", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "blob": { - "name": "blob", - "type": "blob", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "user_aliases": { - "name": "user_aliases", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "user_ref": { - "name": "user_ref", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "alias_name": { - "name": "alias_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "user_aliases_user_ref_users_id_fk": { - "name": "user_aliases_user_ref_users_id_fk", - "tableFrom": "user_aliases", - "tableTo": "users", - "columnsFrom": ["user_ref"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "username": { - "name": "username", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_username_unique": { - "name": "users_username_unique", - "columns": ["username"], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index e801807..dd84a69 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,15 +5,8 @@ { "idx": 0, "version": "6", - "when": 1723761059597, - "tag": "0000_open_famine", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1723979465707, - "tag": "0001_spicy_thena", + "when": 1724016516054, + "tag": "0000_hot_sage", "breakpoints": true } ] diff --git a/package.json b/package.json index 2082e89..0f8ff8c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "@types/eslint": "^9.6.0", "autoprefixer": "^10.4.20", - "bun-types": "^1.1.22", + "bun-types": "^1.1.24", "drizzle-kit": "^0.24.0", "eslint": "^9.9.0", "eslint-plugin-svelte": "^2.43.0", @@ -28,13 +28,13 @@ "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.2.6", "prettier-plugin-tailwindcss": "^0.6.6", - "svelte": "^5.0.0-next.218", + "svelte": "^5.0.0-next.225", "svelte-adapter-bun": "^0.5.2", "svelte-check": "^3.8.5", - "tailwindcss": "^3.4.9", + "tailwindcss": "^3.4.10", "typescript": "^5.5.4", "typescript-eslint": "^8.1.0", - "vite": "^5.4.0" + "vite": "^5.4.1" }, "type": "module", "trustedDependencies": [ diff --git a/src/contrib/migrate.ts b/src/contrib/migrate.ts index 8af5859..270f0d1 100644 --- a/src/contrib/migrate.ts +++ b/src/contrib/migrate.ts @@ -2,4 +2,4 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator"; import { DB } from "../lib/server/db"; -await migrate(DB, { migrationsFolder: "./drizzle" }); +migrate(DB, { migrationsFolder: "./drizzle" }); diff --git a/src/lib/drizzle.ts b/src/lib/drizzle.ts index 46c3e4e..7fa54c4 100644 --- a/src/lib/drizzle.ts +++ b/src/lib/drizzle.ts @@ -22,7 +22,10 @@ export const userTable = sqliteTable("users", { /** * User's Unique ID, assigned by the server, should be a ULID and must not ever change */ - id: text("id", { mode: "text" }).primaryKey().$type(), + id: text("id", { mode: "text" }) + .primaryKey() + .$type() + .$defaultFn(monotonic_ulid), /** * User's display name, this can be used for lookup and can allow a different name to be shown from the username @@ -35,81 +38,70 @@ export const userTable = sqliteTable("users", { * User's Username, can be changed at any time by the user. */ username: text("username", { mode: "text" }).notNull().unique(), + + /** + * User profile picture Asset ID + */ + profile_picture: text("profile_picture").references( + () => publicAssetTable.id, + ), }); export type UserSelectModel = InferSelectModel; export type UserInsertModel = InferInsertModel; -export const userAliasTable = sqliteTable( - "user_aliases", - { - /** - * Alias ID, autoincrementing to keep track of alias count. - */ - id: integer("id").primaryKey({ autoIncrement: true }), - - /** - * User ID, that this alias record will point to - */ - user_ref: text("user_ref", { mode: "text" }).notNull().$type(), - - /** - * Username that this alias points so, helps prevent phishing by having all previous usernames of a user remain cached until the user's account is deleted. - */ - alias_name: text("alias_name", { mode: "text" }).notNull(), - }, - (table) => { - return { - aliasReferences: foreignKey({ - columns: [table.user_ref], - foreignColumns: [userTable.id], - }).onDelete("cascade"), - }; - }, -); +export const userAliasTable = sqliteTable("user_aliases", { + /** + * Alias ID, autoincrementing to keep track of alias count. + */ + id: integer("id").primaryKey({ autoIncrement: true }), + + /** + * User ID, that this alias record will point to + */ + user_ref: text("user_ref", { mode: "text" }) + .notNull() + .$type() + .references(() => userTable.id, { onDelete: "cascade" }), + + /** + * Username that this alias points so, helps prevent phishing by having all previous usernames of a user remain cached until the user's account is deleted. + */ + alias_name: text("alias_name", { mode: "text" }).notNull(), +}); export type UserAliasSelectModel = InferSelectModel; export type UserAliasInsertModel = InferInsertModel; -export const emailAddressesTable = sqliteTable( - "email_addresses", - { - /** - * ID of the email address, user can have multiple email addresses. - * - * And email addresses can be assigned to many users (although requiring validation) - */ - email_id: integer("email_id").primaryKey({ autoIncrement: true }), - - /** - * User's ID that this email record belongs to, since an email address can refer to - * multiple accounts, this is not marked as unique - */ - user_id: text("user_id", { mode: "text", length: 26 }).$type(), - - /** - * email adddress that is associated with this record. - */ - email_address: text("email_address", { mode: "text" }) - .notNull() - .unique(), - - /** - * whether not an email is verified - */ - is_verified: integer("is_verified", { mode: "boolean" }) - .notNull() - .default(sql`0`), - }, - (table) => { - return { - userReference: foreignKey({ - columns: [table.user_id], - foreignColumns: [userTable.id], - }).onDelete("cascade"), - }; - }, -); +export const emailAddressesTable = sqliteTable("email_addresses", { + /** + * ID of the email address, user can have multiple email addresses. + * + * And email addresses can be assigned to many users (although requiring validation) + */ + email_id: integer("email_id").primaryKey({ autoIncrement: true }), + + /** + * User's ID that this email record belongs to, since an email address can refer to + * multiple accounts, this is not marked as unique + */ + user_id: text("user_id", { mode: "text", length: 26 }) + .notNull() + .$type() + .references(() => userTable.id, { onDelete: "cascade" }), + + /** + * email adddress that is associated with this record. + */ + email_address: text("email_address", { mode: "text" }).notNull().unique(), + + /** + * whether not an email is verified + */ + is_verified: integer("is_verified", { mode: "boolean" }) + .notNull() + .default(sql`0`), +}); export type EmailAddressesSelectModel = InferSelectModel< typeof emailAddressesTable @@ -118,6 +110,24 @@ export type EmailAddressesInsertModel = InferInsertModel< typeof emailAddressesTable >; +export const passwordTable = sqliteTable("passwords", { + /** + * User id this password belongs to. + */ + user_id: text("user_id", { mode: "text" }) + .primaryKey() + .references(() => userTable.id, { onDelete: "cascade" }), + /** + * Password Hash + * + * Should be hashed with argon2id + */ + password_hash: text("password_hash").notNull(), +}); + +export type PasswordSelectModel = InferSelectModel; +export type PasswordInsertModel = InferInsertModel; + /** * This is a table that allows us to do a quick and dirty user public assets api * diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 00cc99a..eb13d4b 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -1,7 +1,39 @@ +import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; + import { Database } from "bun:sqlite"; import { drizzle } from "drizzle-orm/bun-sqlite"; -const sqlite = new Database(Bun.env.DATABASE_FILE ?? "./database.db"); +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; + +const sqlite = new Database(Bun.env.DATABASE_FILE ?? "./database.db", { + strict: true, +}); + +// enable WAL, which provides better performance, usually. +sqlite.exec("PRAGMA journal_mode = WAL;"); +// enable foreign key restraints +sqlite.exec("PRAGMA foreign_keys = on;"); + export const DB = drizzle(sqlite, { logger: process.env.NODE_ENV == "development", }); + +/** + * Generate an ephemeral test database, this is an in-memory data store + * used for testing that everything works, the second the database + * variable goes out of scope, the database will be deleted. + * + * @returns { BunSQLiteDatabase } The in-memory test Database + */ +export const ephemeral_test_db = (): BunSQLiteDatabase => { + const db = new Database(":memory:", { strict: true }); + + // enable foreign key restraints + db.exec("PRAGMA foreign_keys = on;"); + + const drizzle_db = drizzle(db); + + migrate(drizzle_db, { migrationsFolder: "./drizzle" }); + + return drizzle_db; +}; diff --git a/src/lib/trpc/router/auth.ts b/src/lib/trpc/router/auth.ts index ec3bb8f..7b74530 100644 --- a/src/lib/trpc/router/auth.ts +++ b/src/lib/trpc/router/auth.ts @@ -2,24 +2,21 @@ import z from "zod"; import { trpcInstance } from "./init"; import { DB } from "$lib/server/db"; -import { userTable, userAliasTable } from "$lib/drizzle"; +import { userTable, userAliasTable, passwordTable } from "$lib/drizzle"; import { eq, or } from "drizzle-orm"; +const USERNAME_SCHEMA = z + .string() + .max(32, "invalid_length") + .regex(/^[a-z0-9_]+$/, "invalid_chars"); + export const authRouter = trpcInstance.router({ + // #region Check Username check_username_availability: trpcInstance.procedure - .input( - z - .string() - .max(32, "invalid_length") - .regex(/^[a-z0-9_]+$/, "invalid_chars"), - ) + .input(USERNAME_SCHEMA) .query(async (opts) => { const username = opts.input; - if (username == "test_user") return { available: false }; - - console.log("checking", JSON.stringify(username)); - const query = await DB.select({ user_id: userTable.id, user_alias_id: userAliasTable.id, @@ -37,4 +34,39 @@ export const authRouter = trpcInstance.router({ available: query.length === 0, }; }), + // #endregion + // #region Sign Up + sign_up: trpcInstance.procedure + .input( + z.object({ + username: USERNAME_SCHEMA, + password: z.string().min(8), + }), + ) + .mutation(async (opts) => { + const { username, password } = opts.input; + + const password_hash = await Bun.password.hash(password, "argon2id"); + + const user_id = await DB.transaction(async (tx) => { + const user = await tx + .insert(userTable) + .values({ + username, + }) + .returning({ user_id: userTable.id }); + + await tx.insert(passwordTable).values({ + user_id: user[0].user_id, + password_hash, + }); + + return user[0].user_id; + }); + + console.log("Created User", user_id); + + // todo: return user token + }), + // #endregion }); diff --git a/src/routes/(auth)/auth/login/+page.svelte b/src/routes/(auth)/auth/login/+page.svelte index 610ada5..c214522 100644 --- a/src/routes/(auth)/auth/login/+page.svelte +++ b/src/routes/(auth)/auth/login/+page.svelte @@ -14,21 +14,18 @@ let password = $state(""); let secret_key_file: File | null = $state(null); - const secret_key_json = $derived.by(async () => { + const secret_key_json: Promise | null = $derived.by(async () => { if (secret_key_file) { const blob_url = URL.createObjectURL(secret_key_file); const blob_response = await fetch(blob_url); return await blob_response.json(); } else { - return null; + return Promise.resolve(null); } }); - $inspect(secret_key_json).with(async (type, val) => - console.log(type, await val), - ); - const onsubmit = (ev: SubmitEvent) => { + ev.stopPropagation(); ev.preventDefault(); }; @@ -100,6 +97,18 @@ + + {#await secret_key_json} + + {:then json} + {#if json} +
+
{JSON.stringify(json, null, 2)}
+
+ {/if} + {/await} diff --git a/src/routes/(auth)/auth/sign-up/+page.svelte b/src/routes/(auth)/auth/sign-up/+page.svelte index f1d1227..3adae69 100644 --- a/src/routes/(auth)/auth/sign-up/+page.svelte +++ b/src/routes/(auth)/auth/sign-up/+page.svelte @@ -4,7 +4,16 @@ import { debounce } from "$lib/utils"; import { PAGE_TRANSITION_TIME } from "$lib"; - import { derived, writable, type Readable } from "svelte/store"; + // renamed because it clashes with the $derived rune? + // ¯\_(ツ)_/¯ + // + // Damn you Rich Harris + import { + derived as derived_store, + get, + writable, + type Readable, + } from "svelte/store"; import { slide, fade } from "svelte/transition"; import { Label } from "@/ui/label"; @@ -14,17 +23,24 @@ import { CircleCheck, CircleX, LoaderCircle } from "lucide-svelte"; import Main from "@/main.svelte"; import { Button } from "@/ui/button"; + import { z } from "zod"; + import { goto } from "$app/navigation"; const rpc = trpc($page); // const utils = rpc.createUtils(); + // #region Username Validation const username = writable(""); - const username_is_empty = derived(username, (val) => val.length == 0, true); + const username_is_empty = derived_store( + username, + (val) => val.length == 0, + true, + ); const username_availability_query = rpc.auth.check_username_availability.createQuery( username, - derived(username_is_empty, (val) => { + derived_store(username_is_empty, (val) => { return { retry: false, enabled: !val, @@ -37,9 +53,8 @@ * Error text generated from zod error, error reasons (used in `reason`) are defined * in the server handler for the `username_availability_query`. */ - const username_invalid_error_text: Readable = derived( - username_availability_query, - (val) => { + const username_invalid_error_text: Readable = + derived_store(username_availability_query, (val) => { if (val.data && val.data.available === false) return "Username is Taken"; @@ -59,14 +74,42 @@ default: return "Unknown Error"; } + }); + + // #endregion + + // #region Password Validation + const PASSWORD_SCHEMA = z + .string() + .min(8, "Password must contain at least 8 characters"); + + let password = $state(""); + + const password_validation: string | boolean = $derived.by(() => { + if (password.length === 0) return false; + + const validity = PASSWORD_SCHEMA.safeParse(password); + if (validity.success) return true; + + return validity.error.format()._errors[0]; + }); + + // #endregion + + const sign_up_mutation = rpc.auth.sign_up.createMutation({ + onSuccess: () => { + goto("/auth/sign-up/onboarding"); }, - ); + }); /** * On Submit form handler */ const onsubmit = (ev: SubmitEvent) => { + ev.stopPropagation(); ev.preventDefault(); + + $sign_up_mutation.mutate({ username: get(username), password }); }; @@ -139,8 +182,31 @@
- - + + + {#if typeof password_validation === "string"} + + {:else if password_validation === true} + + {/if} + + + {#if typeof password_validation === "string"} +

+ {password_validation} +

+ {/if}
@@ -151,7 +217,9 @@ Login - +
diff --git a/src/routes/(auth)/auth/sign-up/onboarding/+page.svelte b/src/routes/(auth)/auth/sign-up/onboarding/+page.svelte new file mode 100644 index 0000000..7fb6d82 --- /dev/null +++ b/src/routes/(auth)/auth/sign-up/onboarding/+page.svelte @@ -0,0 +1,7 @@ + + +
+

Onboarding

+
diff --git a/src/test/index.test.ts b/src/test/index.test.ts deleted file mode 100644 index 1aa4361..0000000 --- a/src/test/index.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -describe("Ensure Bun is Functioning", () => { - test("Basic addition", () => { - expect(1 + 1).toEqual(2); - }); -}); diff --git a/src/test/signup.test.ts b/src/test/signup.test.ts new file mode 100644 index 0000000..c918253 --- /dev/null +++ b/src/test/signup.test.ts @@ -0,0 +1,50 @@ +import { describe, test, expect } from "bun:test"; +import { SQLiteError } from "bun:sqlite"; + +import { ephemeral_test_db } from "$lib/server/db"; +import { userTable, passwordTable } from "$lib/drizzle"; +import { ulid } from "ulid"; + +describe("Sign Up Functionality", () => { + test("Password without a UserID should fail", async () => { + const db = ephemeral_test_db(); + + const test_function = async () => + await db + .insert(passwordTable) + .values({ user_id: "test_user_id", password_hash: "aaaaaa" }); + + expect(test_function).toThrowError(SQLiteError); + + const password_table_rows = await db.select().from(passwordTable); + + expect(password_table_rows).toBeEmpty(); + }); + + test("Password with a UserID should succeed", async () => { + const TEST_USER_ID = ulid(); + const TEST_PASSWORD_HASH = + "$argon2id$v=19$m=16,t=2,p=1$c2RmYWFkc2ZkZmFzZGZhc2Rm$Smi6y+dcAPZn/36OGK6tUw"; + + const db = ephemeral_test_db(); + + const test_promise = db.transaction(async (tx) => { + const user = await tx + .insert(userTable) + .values({ + id: TEST_USER_ID, + username: "test_user", + }) + .returning(); + + await tx.insert(passwordTable).values({ + user_id: user[0].id, + password_hash: TEST_PASSWORD_HASH, + }); + + return user[0].id; + }); + + expect(test_promise).resolves.toBe(TEST_USER_ID); + }); +});