Fix auth hook so it returns forbidden if the user is authenticated but not allowed to hit an endpoint.
This commit is contained in:
		
							
								
								
									
										2
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
// See https://svelte.dev/docs/kit/types#app.d.ts
 | 
			
		||||
 | 
			
		||||
import type { LocalCredentials } from "$lib/server/requestTools";
 | 
			
		||||
import type { LocalCredentials } from "$lib/server/getRequestBody";
 | 
			
		||||
 | 
			
		||||
// for information about these interfaces
 | 
			
		||||
declare global {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +0,0 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
 | 
			
		||||
describe('sum test', () => {
 | 
			
		||||
	it('adds 1 + 2 to equal 3', () => {
 | 
			
		||||
		expect(1 + 2).toBe(3);
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
@ -1,15 +1,29 @@
 | 
			
		||||
import { isAuthorized } from "$lib/server/requestTools";
 | 
			
		||||
import { unauthorizedResponse } from "$lib/server/responseBodies";
 | 
			
		||||
import type { Handle } from "@sveltejs/kit";
 | 
			
		||||
import * as auth from "$lib/server/auth";
 | 
			
		||||
import { forbiddenResponse, unauthorizedResponse } from "$lib/server/responseBodies";
 | 
			
		||||
import { routeAuth, type Method } from "$lib/server/routeAuth";
 | 
			
		||||
import { type Handle } from "@sveltejs/kit";
 | 
			
		||||
 | 
			
		||||
export const handle: Handle = async ({ event, resolve }) => {
 | 
			
		||||
	const auth = await isAuthorized(event);
 | 
			
		||||
	const creds = await auth.authenticate(event);
 | 
			
		||||
 | 
			
		||||
	if (!auth) {
 | 
			
		||||
	if (!creds) {
 | 
			
		||||
		return unauthorizedResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	event.locals.user = auth;
 | 
			
		||||
	const authResult = auth.isAuthorized(
 | 
			
		||||
		routeAuth,
 | 
			
		||||
		event.request.method as Method,
 | 
			
		||||
		event.url.pathname,
 | 
			
		||||
		creds,
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	if (authResult === auth.AuthorizationResult.Denied) {
 | 
			
		||||
		return forbiddenResponse();
 | 
			
		||||
	} else if (authResult === auth.AuthorizationResult.Unauthenticated) {
 | 
			
		||||
		return unauthorizedResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	event.locals.user = creds;
 | 
			
		||||
 | 
			
		||||
	return await resolve(event);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { hasOnlyKeys, hasProperty } from "./validation";
 | 
			
		||||
import type { Id } from "./Id";
 | 
			
		||||
import { isId, type Id } from "./Id";
 | 
			
		||||
import type { State } from "./State";
 | 
			
		||||
 | 
			
		||||
export interface GameData {
 | 
			
		||||
@ -13,7 +13,15 @@ export function isGameData(target: unknown): target is GameData {
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!hasProperty(target, "players", "string[]")) {
 | 
			
		||||
	if ("players" in (target as any)) {
 | 
			
		||||
		const { players } = target as any;
 | 
			
		||||
 | 
			
		||||
		for (const player of players) {
 | 
			
		||||
			if (!isId(player)) {
 | 
			
		||||
				return false;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -174,9 +174,7 @@ export class RollForFirst implements GameEvent {
 | 
			
		||||
				state.dieCount = 6;
 | 
			
		||||
			} else {
 | 
			
		||||
				// ...otherwise, setup for tie breaking rolls.
 | 
			
		||||
				state.scores = scores.map((_, i) =>
 | 
			
		||||
					ties.has(i) ? FIRST_ROLL_PENDING : FIRST_ROLL_LOST,
 | 
			
		||||
				);
 | 
			
		||||
				state.scores = scores.map((_, i) => (ties.has(i) ? FIRST_ROLL_PENDING : FIRST_ROLL_LOST));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -368,9 +366,7 @@ export class Score implements GameEvent {
 | 
			
		||||
		// Increment the index of the active player, circling back to 1 if the player
 | 
			
		||||
		// who just scored was the last player in the array.
 | 
			
		||||
		state.playing =
 | 
			
		||||
			playerCount - 1 === this.player
 | 
			
		||||
				? (state.playing = 0)
 | 
			
		||||
				: (state.playing = this.player + 1);
 | 
			
		||||
			playerCount - 1 === this.player ? (state.playing = 0) : (state.playing = this.player + 1);
 | 
			
		||||
 | 
			
		||||
		state.dieCount = 6;
 | 
			
		||||
		delete state.heldScore;
 | 
			
		||||
@ -387,7 +383,10 @@ export class Score implements GameEvent {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function scorePips(count: number, pips: number) {
 | 
			
		||||
	if (coa
 | 
			
		||||
	if (count < 3) {
 | 
			
		||||
		// If not a three of a kind, return the raw dice value...
 | 
			
		||||
		return pipScore(pips) * count;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// ...otherwise, this is a three or more of a kind.
 | 
			
		||||
	if (pips === 1) {
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,14 @@ export function createId(): Id {
 | 
			
		||||
	return new ObjectId();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function idFromString(str: string) {
 | 
			
		||||
	return new ObjectId(str);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stringFromId(id: Id) {
 | 
			
		||||
	return id.toString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isId(target: unknown): target is Id {
 | 
			
		||||
	return target instanceof ObjectId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,31 +1,35 @@
 | 
			
		||||
import { JWT_SECRET } from "$env/static/private";
 | 
			
		||||
import type { RequestEvent } from "@sveltejs/kit";
 | 
			
		||||
import jwt from "jsonwebtoken";
 | 
			
		||||
import { routeAuth, type Method, type RouteAuthRule } from "./routeAuth";
 | 
			
		||||
import { type Method, type RouteAuthRule } from "./routeAuth";
 | 
			
		||||
import type { Listing } from "$lib/Listing";
 | 
			
		||||
import type { LoginData } from "$lib/Login";
 | 
			
		||||
 | 
			
		||||
export type LocalCredentials =
 | 
			
		||||
export type LocalCredentials = (
 | 
			
		||||
	| { kind: "Basic"; payload: { username: string; password: string } }
 | 
			
		||||
	| { kind: "Bearer"; payload: jwt.JwtPayload | string }
 | 
			
		||||
	| { kind: "None" };
 | 
			
		||||
	| { kind: "None" }
 | 
			
		||||
) & { role: string };
 | 
			
		||||
 | 
			
		||||
export async function getRequestBody<T = unknown>(
 | 
			
		||||
	req: Request,
 | 
			
		||||
	validation?: (target: unknown) => target is T
 | 
			
		||||
): Promise<T> {
 | 
			
		||||
	if (req.body === null) {
 | 
			
		||||
		throw new Error("no body is present on the request");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const body = await req.json();
 | 
			
		||||
 | 
			
		||||
	if (validation && !validation(body)) {
 | 
			
		||||
		throw new Error("body validation failed");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return body;
 | 
			
		||||
export enum AuthorizationResult {
 | 
			
		||||
	Allowed,
 | 
			
		||||
	Denied,
 | 
			
		||||
	Unauthenticated,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function isAuthorized(event: RequestEvent): Promise<LocalCredentials | null> {
 | 
			
		||||
export async function createToken(listing: Listing<LoginData>) {
 | 
			
		||||
	return await jwt.sign(
 | 
			
		||||
		{ sub: listing.id, username: listing.data.username, role: listing.data.role },
 | 
			
		||||
		JWT_SECRET,
 | 
			
		||||
		{
 | 
			
		||||
			expiresIn: "1d",
 | 
			
		||||
		},
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function authenticate(
 | 
			
		||||
	event: RequestEvent,
 | 
			
		||||
): Promise<LocalCredentials | null> {
 | 
			
		||||
	let path = event.url.pathname;
 | 
			
		||||
	let tokenKind: "Basic" | "Bearer" | "None";
 | 
			
		||||
	let tokenRole: string;
 | 
			
		||||
@ -35,7 +39,7 @@ export async function isAuthorized(event: RequestEvent): Promise<LocalCredential
 | 
			
		||||
 | 
			
		||||
	if (parts[0] !== "api") {
 | 
			
		||||
		// not concerned about requests made to the frontend server
 | 
			
		||||
		return { kind: "None" };
 | 
			
		||||
		return { kind: "None", role: "default" };
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const authHeader = event.request.headers.get("authorization");
 | 
			
		||||
@ -46,7 +50,7 @@ export async function isAuthorized(event: RequestEvent): Promise<LocalCredential
 | 
			
		||||
		// role.
 | 
			
		||||
		tokenKind = "None";
 | 
			
		||||
		tokenRole = "default";
 | 
			
		||||
		tokenDesc = { kind: "None" };
 | 
			
		||||
		tokenDesc = { kind: "None", role: tokenRole };
 | 
			
		||||
	} else {
 | 
			
		||||
		const [kind, token] = authHeader.split(" ");
 | 
			
		||||
 | 
			
		||||
@ -64,7 +68,7 @@ export async function isAuthorized(event: RequestEvent): Promise<LocalCredential
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			tokenRole = payload.role;
 | 
			
		||||
			tokenDesc = { kind: "Bearer", payload };
 | 
			
		||||
			tokenDesc = { kind: "Bearer", payload, role: tokenRole };
 | 
			
		||||
 | 
			
		||||
			if (!tokenRole) {
 | 
			
		||||
				// Something has gone wrong: I should not have issued a token without a
 | 
			
		||||
@ -78,36 +82,46 @@ export async function isAuthorized(event: RequestEvent): Promise<LocalCredential
 | 
			
		||||
			tokenKind = "Basic";
 | 
			
		||||
			tokenRole = "default";
 | 
			
		||||
 | 
			
		||||
			tokenDesc = { kind: "Basic", payload: { username, password } };
 | 
			
		||||
			tokenDesc = { kind: "Basic", payload: { username, password }, role: tokenRole };
 | 
			
		||||
		} else {
 | 
			
		||||
			// Something the server doesn't recognize was passed as the token kind.
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tokenDesc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isAuthorized(
 | 
			
		||||
	roleRules: { [k: string]: RouteAuthRule[] },
 | 
			
		||||
	method: Method,
 | 
			
		||||
	path: string,
 | 
			
		||||
	creds: LocalCredentials,
 | 
			
		||||
): AuthorizationResult {
 | 
			
		||||
	// Determine if the role has the ability to hit this endpoint with this method
 | 
			
		||||
	// and the given token.
 | 
			
		||||
	const rules = routeAuth[tokenRole];
 | 
			
		||||
	const parts = breakupPath(path);
 | 
			
		||||
	const { role: tokenRole, kind: tokenKind } = creds;
 | 
			
		||||
	const rules = roleRules[tokenRole];
 | 
			
		||||
 | 
			
		||||
	let hasMatchingAllow = false;
 | 
			
		||||
	for (const rule of rules) {
 | 
			
		||||
		console.log("checking rule", rule);
 | 
			
		||||
		if (matchesRequest(tokenKind, parts, event.request.method as Method, rule)) {
 | 
			
		||||
			console.log("match!");
 | 
			
		||||
			if (rule.action === "deny") {
 | 
			
		||||
		if (matchesRequest(parts, method, rule)) {
 | 
			
		||||
			if (tokenKind !== (rule.tokenKind ?? "Bearer")) {
 | 
			
		||||
				return AuthorizationResult.Unauthenticated;
 | 
			
		||||
			} else if (rule.action === "deny") {
 | 
			
		||||
				// if a request matches any deny rule, then it is denied, regardless
 | 
			
		||||
				// of whether or not it also matches an allow rule.
 | 
			
		||||
				return null;
 | 
			
		||||
				return AuthorizationResult.Denied;
 | 
			
		||||
			} else if (rule.action === "allow") {
 | 
			
		||||
				hasMatchingAllow = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (hasMatchingAllow) {
 | 
			
		||||
		return tokenDesc;
 | 
			
		||||
	}
 | 
			
		||||
	return null;
 | 
			
		||||
	if (hasMatchingAllow) return AuthorizationResult.Allowed;
 | 
			
		||||
	if (tokenKind === "Bearer") return AuthorizationResult.Denied;
 | 
			
		||||
	else return AuthorizationResult.Unauthenticated;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function breakupPath(path: string): string[] {
 | 
			
		||||
@ -118,18 +132,11 @@ function breakupPath(path: string): string[] {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function matchesRequest(
 | 
			
		||||
	requestTokenKind: "Basic" | "Bearer" | "None",
 | 
			
		||||
	requestParts: string[],
 | 
			
		||||
	method: Method,
 | 
			
		||||
	{ endpoint, methods, tokenKind = "Bearer" }: RouteAuthRule
 | 
			
		||||
	{ endpoint, methods }: RouteAuthRule,
 | 
			
		||||
): boolean {
 | 
			
		||||
	if (tokenKind !== requestTokenKind) {
 | 
			
		||||
		console.log("token types didn't match", tokenKind, requestTokenKind);
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!methods.includes("*") && !methods.includes(method)) {
 | 
			
		||||
		console.log("Bad method", method);
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -148,7 +155,6 @@ function matchesRequest(
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (rulePart !== reqPart) {
 | 
			
		||||
			console.log("rule parts do not match", rulePart, reqPart);
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
							
								
								
									
										16
									
								
								src/lib/server/getRequestBody.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/lib/server/getRequestBody.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
export async function getRequestBody<T = unknown>(
 | 
			
		||||
	req: Request,
 | 
			
		||||
	validation?: (target: unknown) => target is T,
 | 
			
		||||
): Promise<T> {
 | 
			
		||||
	if (req.body === null) {
 | 
			
		||||
		throw new Error("no body is present on the request");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const body = await req.json();
 | 
			
		||||
 | 
			
		||||
	if (validation && !validation(body)) {
 | 
			
		||||
		throw new Error("body validation failed");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return body;
 | 
			
		||||
}
 | 
			
		||||
@ -13,13 +13,13 @@ export const routeAuth: { [k: string]: RouteAuthRule[] } = {
 | 
			
		||||
	// a Basic token. Other than that, they cannot do anything!
 | 
			
		||||
	default: [
 | 
			
		||||
		{ action: "allow", methods: ["POST"], endpoint: "/api/users", tokenKind: "None" },
 | 
			
		||||
		{ action: "allow", methods: ["POST"], endpoint: "/api/token", tokenKind: "Basic" }
 | 
			
		||||
		{ action: "allow", methods: ["POST"], endpoint: "/api/token", tokenKind: "Basic" },
 | 
			
		||||
	],
 | 
			
		||||
 | 
			
		||||
	// player is anyone else. They are authorized to hit any endpoint, using any method,
 | 
			
		||||
	// with a Bearer token.
 | 
			
		||||
	player: [
 | 
			
		||||
		{ action: "allow", methods: ["*"], endpoint: "*" },
 | 
			
		||||
		{ action: "deny", methods: ["POST"], endpoint: "/api/token" }
 | 
			
		||||
	]
 | 
			
		||||
		{ action: "deny", methods: ["POST"], endpoint: "/api/token" },
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,19 @@
 | 
			
		||||
import { describe, it } from "node:test";
 | 
			
		||||
import { Game } from "../../Game";
 | 
			
		||||
import { describe, it } from "vitest";
 | 
			
		||||
import { Game } from "$lib/server/Game";
 | 
			
		||||
import { deepEqual, ok, throws } from "node:assert/strict";
 | 
			
		||||
import { createId, idFromString, stringFromId } from "$lib/Id";
 | 
			
		||||
import { equal } from "node:assert";
 | 
			
		||||
 | 
			
		||||
describe("Game", () => {
 | 
			
		||||
	const idString = stringFromId(createId());
 | 
			
		||||
	describe("addPlayer", () => {
 | 
			
		||||
		it("should push a player id into the player array", () => {
 | 
			
		||||
			const game = new Game();
 | 
			
		||||
			deepEqual(game.players, []);
 | 
			
		||||
 | 
			
		||||
			game.addPlayer("some-id");
 | 
			
		||||
			deepEqual(game.players, ["some-id"]);
 | 
			
		||||
			game.addPlayer(idFromString(idString));
 | 
			
		||||
			equal(game.players.length, 1);
 | 
			
		||||
			equal(stringFromId(game.players[0]), idString);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@ -1,26 +1,29 @@
 | 
			
		||||
import { describe, it } from "node:test";
 | 
			
		||||
import { GameData, isGameData } from "../../GameData";
 | 
			
		||||
import { describe, it } from "vitest";
 | 
			
		||||
import { type GameData, isGameData } from "$lib/GameData";
 | 
			
		||||
import { equal, ok } from "node:assert/strict";
 | 
			
		||||
import { createId, idFromString, stringFromId } from "$lib/Id";
 | 
			
		||||
 | 
			
		||||
describe("GameData", () => {
 | 
			
		||||
	const idString = stringFromId(createId());
 | 
			
		||||
 | 
			
		||||
	describe("isGameData", () => {
 | 
			
		||||
		it("rejects a malformed object", () => {
 | 
			
		||||
			let data: unknown = {
 | 
			
		||||
				players: ["id", 3],
 | 
			
		||||
				players: [idFromString(idString), idString],
 | 
			
		||||
				isStarted: false,
 | 
			
		||||
				state: {},
 | 
			
		||||
			};
 | 
			
		||||
			equal(isGameData(data), false);
 | 
			
		||||
 | 
			
		||||
			data = {
 | 
			
		||||
				players: ["id"],
 | 
			
		||||
				players: [idFromString(idString)],
 | 
			
		||||
				isStarted: null,
 | 
			
		||||
				state: {},
 | 
			
		||||
			};
 | 
			
		||||
			equal(isGameData(data), false);
 | 
			
		||||
 | 
			
		||||
			data = {
 | 
			
		||||
				players: ["id"],
 | 
			
		||||
				players: [idFromString(idString)],
 | 
			
		||||
				isStarted: false,
 | 
			
		||||
			};
 | 
			
		||||
			equal(isGameData(data), false);
 | 
			
		||||
@ -28,7 +31,7 @@ describe("GameData", () => {
 | 
			
		||||
 | 
			
		||||
		it("rejects an object with extra properties", () => {
 | 
			
		||||
			const data: GameData & { extra: boolean } = {
 | 
			
		||||
				players: ["id"],
 | 
			
		||||
				players: [idFromString(idString)],
 | 
			
		||||
				isStarted: false,
 | 
			
		||||
				state: {},
 | 
			
		||||
				extra: true,
 | 
			
		||||
@ -39,7 +42,7 @@ describe("GameData", () => {
 | 
			
		||||
 | 
			
		||||
		it("should accept a proper GameData object", () => {
 | 
			
		||||
			const data: GameData = {
 | 
			
		||||
				players: ["id"],
 | 
			
		||||
				players: [idFromString(idString)],
 | 
			
		||||
				state: {},
 | 
			
		||||
				isStarted: false,
 | 
			
		||||
			};
 | 
			
		||||
@ -12,19 +12,14 @@ import {
 | 
			
		||||
} from "../../GameEvent";
 | 
			
		||||
import type { GameEventData } from "../../GameEvent";
 | 
			
		||||
import type { GameData } from "../../GameData";
 | 
			
		||||
import { describe, it } from "node:test";
 | 
			
		||||
import { describe, it } from "vitest";
 | 
			
		||||
import type { State } from "../../State";
 | 
			
		||||
import { doesNotThrow, deepStrictEqual, equal, ok, throws } from "assert";
 | 
			
		||||
import { createId, idFromString, stringFromId } from "$lib/Id";
 | 
			
		||||
 | 
			
		||||
describe("Game Events", () => {
 | 
			
		||||
	describe("isGameEventData", () => {
 | 
			
		||||
		it("should return false if the target is not an object", () => {
 | 
			
		||||
			// const target = {
 | 
			
		||||
			// 	kind: GameEventKind.Hold,
 | 
			
		||||
			// 	player: 0,
 | 
			
		||||
			// 	value: [1, 4],
 | 
			
		||||
			// };
 | 
			
		||||
 | 
			
		||||
			equal(isGameEventData("target"), false);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@ -60,10 +55,13 @@ describe("Game Events", () => {
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe("getGameEvent", () => {
 | 
			
		||||
		const idString = stringFromId(createId());
 | 
			
		||||
		const anotherIdString = stringFromId(createId());
 | 
			
		||||
 | 
			
		||||
		it("should throw if the kind is unkown", () => {
 | 
			
		||||
			const data: GameData = {
 | 
			
		||||
				isStarted: false,
 | 
			
		||||
				players: ["42", "1,"],
 | 
			
		||||
				players: [idFromString(idString), idFromString(anotherIdString)],
 | 
			
		||||
				state: {},
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
@ -79,7 +77,7 @@ describe("Game Events", () => {
 | 
			
		||||
		it("should throw when SeatPlayers has the wrong number of players", () => {
 | 
			
		||||
			const data: GameData = {
 | 
			
		||||
				isStarted: true,
 | 
			
		||||
				players: ["42", "1,"],
 | 
			
		||||
				players: [idFromString(idString), idFromString(anotherIdString)],
 | 
			
		||||
				state: {},
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
@ -94,7 +92,7 @@ describe("Game Events", () => {
 | 
			
		||||
		it("should return a SeatPlayers object when the number of players is correct", () => {
 | 
			
		||||
			const data: GameData = {
 | 
			
		||||
				isStarted: true,
 | 
			
		||||
				players: ["42", "1,"],
 | 
			
		||||
				players: [idFromString(idString), idFromString(anotherIdString)],
 | 
			
		||||
				state: {},
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
@ -109,7 +107,7 @@ describe("Game Events", () => {
 | 
			
		||||
		it("should throw an error if the player passes a full roll with Roll", () => {
 | 
			
		||||
			const data: GameData = {
 | 
			
		||||
				isStarted: true,
 | 
			
		||||
				players: ["42", "1,"],
 | 
			
		||||
				players: [idFromString(idString), idFromString(anotherIdString)],
 | 
			
		||||
				state: {},
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
@ -125,7 +123,7 @@ describe("Game Events", () => {
 | 
			
		||||
		it("should return a Roll object with dice values when the player passes a die count as a value", () => {
 | 
			
		||||
			const data: GameData = {
 | 
			
		||||
				isStarted: true,
 | 
			
		||||
				players: ["42", "1,"],
 | 
			
		||||
				players: [idFromString(idString), idFromString(anotherIdString)],
 | 
			
		||||
				state: {},
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
@ -145,7 +143,7 @@ describe("Game Events", () => {
 | 
			
		||||
		it("should return the class that corresponds with a given kind", () => {
 | 
			
		||||
			const data: GameData = {
 | 
			
		||||
				isStarted: true,
 | 
			
		||||
				players: ["42", "1,"],
 | 
			
		||||
				players: [idFromString(idString), idFromString(anotherIdString)],
 | 
			
		||||
				state: {},
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
@ -222,10 +220,7 @@ describe("Game Events", () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			it("should throw if the value is not a number", () => {
 | 
			
		||||
				throws(
 | 
			
		||||
					() =>
 | 
			
		||||
						new RollForFirst({ kind: GameEventKind.RollForFirst, player: 0, value: [4] }),
 | 
			
		||||
				);
 | 
			
		||||
				throws(() => new RollForFirst({ kind: GameEventKind.RollForFirst, player: 0, value: [4] }));
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@ -267,7 +262,6 @@ describe("Game Events", () => {
 | 
			
		||||
				const state: State = { scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING] };
 | 
			
		||||
 | 
			
		||||
				throws(() => ev.run(state));
 | 
			
		||||
				console.log("done");
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			it("should throw if the player has already rolled", () => {
 | 
			
		||||
@ -296,12 +290,7 @@ describe("Game Events", () => {
 | 
			
		||||
 | 
			
		||||
			it("should reset the scores and set the winning player when everyone has rolled", () => {
 | 
			
		||||
				const state: State = {
 | 
			
		||||
					scores: [
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
					],
 | 
			
		||||
					scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING],
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				let ev = new RollForFirst({
 | 
			
		||||
@ -324,24 +313,14 @@ describe("Game Events", () => {
 | 
			
		||||
 | 
			
		||||
				deepStrictEqual(state, {
 | 
			
		||||
					dieCount: 6,
 | 
			
		||||
					scores: [
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
					],
 | 
			
		||||
					scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING],
 | 
			
		||||
					playing: 2,
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			it("should reset tied players for tie breaker", () => {
 | 
			
		||||
				const state: State = {
 | 
			
		||||
					scores: [
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
					],
 | 
			
		||||
					scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING],
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				let ev = new RollForFirst({
 | 
			
		||||
@ -361,23 +340,13 @@ describe("Game Events", () => {
 | 
			
		||||
				ev.run(state);
 | 
			
		||||
 | 
			
		||||
				deepStrictEqual(state, {
 | 
			
		||||
					scores: [
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_LOST,
 | 
			
		||||
						FIRST_ROLL_LOST,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
					],
 | 
			
		||||
					scores: [FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_LOST, FIRST_ROLL_PENDING],
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			it("should throw if a player whose lost tries to roll again", () => {
 | 
			
		||||
				const state: State = {
 | 
			
		||||
					scores: [
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_LOST,
 | 
			
		||||
						FIRST_ROLL_LOST,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
					],
 | 
			
		||||
					scores: [FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_LOST, FIRST_ROLL_PENDING],
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				const ev = new RollForFirst({
 | 
			
		||||
@ -390,12 +359,7 @@ describe("Game Events", () => {
 | 
			
		||||
 | 
			
		||||
			it("should allow tied players to keep rolling until somoene wins", () => {
 | 
			
		||||
				const state: State = {
 | 
			
		||||
					scores: [
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
						FIRST_ROLL_LOST,
 | 
			
		||||
						FIRST_ROLL_PENDING,
 | 
			
		||||
					],
 | 
			
		||||
					scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_PENDING],
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				// simulate another 3-way tie
 | 
			
		||||
@ -415,12 +379,7 @@ describe("Game Events", () => {
 | 
			
		||||
				deepStrictEqual(
 | 
			
		||||
					state,
 | 
			
		||||
					{
 | 
			
		||||
						scores: [
 | 
			
		||||
							FIRST_ROLL_PENDING,
 | 
			
		||||
							FIRST_ROLL_PENDING,
 | 
			
		||||
							FIRST_ROLL_LOST,
 | 
			
		||||
							FIRST_ROLL_PENDING,
 | 
			
		||||
						],
 | 
			
		||||
						scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_PENDING],
 | 
			
		||||
					},
 | 
			
		||||
					"shouldn't change in a 3-way tie",
 | 
			
		||||
				);
 | 
			
		||||
@ -438,12 +397,7 @@ describe("Game Events", () => {
 | 
			
		||||
				deepStrictEqual(
 | 
			
		||||
					state,
 | 
			
		||||
					{
 | 
			
		||||
						scores: [
 | 
			
		||||
							FIRST_ROLL_PENDING,
 | 
			
		||||
							FIRST_ROLL_LOST,
 | 
			
		||||
							FIRST_ROLL_LOST,
 | 
			
		||||
							FIRST_ROLL_PENDING,
 | 
			
		||||
						],
 | 
			
		||||
						scores: [FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_LOST, FIRST_ROLL_PENDING],
 | 
			
		||||
					},
 | 
			
		||||
					"should update for a smaller tie",
 | 
			
		||||
				);
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
import { describe, it } from "node:test";
 | 
			
		||||
import { describe, it } from "vitest";
 | 
			
		||||
import { createNewListing, updateListing } from "../modifyListing";
 | 
			
		||||
import { Game } from "../../Game";
 | 
			
		||||
import { Game } from "$lib/server/Game";
 | 
			
		||||
import { deepEqual, equal, ok } from "node:assert/strict";
 | 
			
		||||
import { isId } from "$lib/Id";
 | 
			
		||||
 | 
			
		||||
describe("Listing", () => {
 | 
			
		||||
	describe("createNewListing", () => {
 | 
			
		||||
@ -10,10 +11,10 @@ describe("Listing", () => {
 | 
			
		||||
			const listing = createNewListing(game);
 | 
			
		||||
 | 
			
		||||
			ok(listing.data instanceof Game);
 | 
			
		||||
			ok(listing.createdAt instanceof Date);
 | 
			
		||||
			ok(listing.modifiedAt === null);
 | 
			
		||||
			ok(!listing.deleted);
 | 
			
		||||
			equal(typeof listing.id, "string");
 | 
			
		||||
			ok(isId(listing.id));
 | 
			
		||||
			equal(typeof listing.createdAt, "string");
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@ -27,7 +28,7 @@ describe("Listing", () => {
 | 
			
		||||
			const updatedListing = updateListing(listing, update);
 | 
			
		||||
 | 
			
		||||
			deepEqual(updatedListing.data, update);
 | 
			
		||||
			ok(updatedListing.modifiedAt instanceof Date);
 | 
			
		||||
			equal(typeof updatedListing.modifiedAt, "string");
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { describe, it } from "node:test";
 | 
			
		||||
import { getDiceRoll } from "../../getDiceRoll";
 | 
			
		||||
import { describe, it } from "vitest";
 | 
			
		||||
import { getDiceRoll } from "$lib/server/getDiceRoll";
 | 
			
		||||
import { deepEqual } from "node:assert/strict";
 | 
			
		||||
 | 
			
		||||
function testRandom() {
 | 
			
		||||
@ -11,5 +11,8 @@ describe("getDiceRoll", () => {
 | 
			
		||||
	it("should return an array of numbers from 1 to 6 with a given length", () => {
 | 
			
		||||
		let rand = getDiceRoll(6, testRandom());
 | 
			
		||||
		deepEqual(rand, [0, 1, 3, 3, 4, 6]);
 | 
			
		||||
 | 
			
		||||
		rand = getDiceRoll(3, testRandom());
 | 
			
		||||
		deepEqual(rand, [0, 1, 3]);
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { describe, it } from "node:test";
 | 
			
		||||
import { describe, it } from "vitest";
 | 
			
		||||
import { equal, ok } from "node:assert/strict";
 | 
			
		||||
import { hasProperty, hasOnlyKeys } from "../../validation";
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ describe("validation", () => {
 | 
			
		||||
				third: false,
 | 
			
		||||
				fourth: null,
 | 
			
		||||
				fifth: { something: "important" },
 | 
			
		||||
				sixth: ["one", "two"],
 | 
			
		||||
				sixth: ["one", "two"]
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			ok(hasProperty(target, "first", "string"));
 | 
			
		||||
@ -59,7 +59,7 @@ describe("validation", () => {
 | 
			
		||||
 | 
			
		||||
		it("should return false if passed an array type and the property isn't an array", () => {
 | 
			
		||||
			const target = {
 | 
			
		||||
				arr: "not array",
 | 
			
		||||
				arr: "not array"
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			equal(hasProperty(target, "arr", "string[]"), false);
 | 
			
		||||
@ -67,7 +67,7 @@ describe("validation", () => {
 | 
			
		||||
 | 
			
		||||
		it("should return false if the defined array contains a non-matching element", () => {
 | 
			
		||||
			const target = {
 | 
			
		||||
				arr: ["I", "was", "born", "in", 1989],
 | 
			
		||||
				arr: ["I", "was", "born", "in", 1989]
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			equal(hasProperty(target, "arr", "string[]"), false);
 | 
			
		||||
@ -75,7 +75,7 @@ describe("validation", () => {
 | 
			
		||||
 | 
			
		||||
		it("should return true if all the elements in a defined array match", () => {
 | 
			
		||||
			const target = {
 | 
			
		||||
				arr: ["I", "was", "born", "in", "1989"],
 | 
			
		||||
				arr: ["I", "was", "born", "in", "1989"]
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			ok(hasProperty(target, "arr", "string[]"));
 | 
			
		||||
@ -83,7 +83,7 @@ describe("validation", () => {
 | 
			
		||||
 | 
			
		||||
		it("should return true if all the elements in a defined array match one of multiple types", () => {
 | 
			
		||||
			const target = {
 | 
			
		||||
				arr: ["I", "was", "born", "in", 1989],
 | 
			
		||||
				arr: ["I", "was", "born", "in", 1989]
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			ok(hasProperty(target, "arr", "(string|number)[]"));
 | 
			
		||||
@ -91,7 +91,7 @@ describe("validation", () => {
 | 
			
		||||
 | 
			
		||||
		it("should return true if type is null but property is nullable", () => {
 | 
			
		||||
			const target = {
 | 
			
		||||
				nullable: null,
 | 
			
		||||
				nullable: null
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			ok(hasProperty(target, "nullable", "string", true));
 | 
			
		||||
@ -107,7 +107,7 @@ describe("validation", () => {
 | 
			
		||||
			const target = {
 | 
			
		||||
				one: "one",
 | 
			
		||||
				two: "two",
 | 
			
		||||
				three: "three",
 | 
			
		||||
				three: "three"
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			const keys = ["one", "two"];
 | 
			
		||||
@ -119,7 +119,7 @@ describe("validation", () => {
 | 
			
		||||
			const target = {
 | 
			
		||||
				one: "one",
 | 
			
		||||
				two: "two",
 | 
			
		||||
				three: "three",
 | 
			
		||||
				three: "three"
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			const keys = ["one", "two", "three"];
 | 
			
		||||
@ -129,7 +129,7 @@ describe("validation", () => {
 | 
			
		||||
 | 
			
		||||
		it("should return true if the target has only a subset of the provided keys", () => {
 | 
			
		||||
			const target = {
 | 
			
		||||
				one: "one",
 | 
			
		||||
				one: "one"
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			const keys = ["one", "two", "three"];
 | 
			
		||||
@ -1,17 +1,15 @@
 | 
			
		||||
import { isListing } from "$lib/Listing";
 | 
			
		||||
import { isLoginData } from "$lib/Login";
 | 
			
		||||
import { readListingByQuery } from "$lib/server/mongo";
 | 
			
		||||
import { getRequestBody } from "$lib/server/requestTools";
 | 
			
		||||
import {
 | 
			
		||||
	badRequestResponse,
 | 
			
		||||
	notFoundResponse,
 | 
			
		||||
	singleResponse,
 | 
			
		||||
	unauthorizedResponse
 | 
			
		||||
	unauthorizedResponse,
 | 
			
		||||
} from "$lib/server/responseBodies";
 | 
			
		||||
import type { RequestHandler } from "@sveltejs/kit";
 | 
			
		||||
import { JWT_SECRET } from "$env/static/private";
 | 
			
		||||
import { compare } from "bcrypt";
 | 
			
		||||
import jwt from "jsonwebtoken";
 | 
			
		||||
import { createToken } from "$lib/server/auth";
 | 
			
		||||
 | 
			
		||||
export const POST: RequestHandler = async ({ locals }): Promise<Response> => {
 | 
			
		||||
	try {
 | 
			
		||||
@ -25,9 +23,9 @@ export const POST: RequestHandler = async ({ locals }): Promise<Response> => {
 | 
			
		||||
		const listing = await readListingByQuery(
 | 
			
		||||
			"logins",
 | 
			
		||||
			{
 | 
			
		||||
				"data.username": username
 | 
			
		||||
				"data.username": username,
 | 
			
		||||
			},
 | 
			
		||||
			(target) => isListing(target, isLoginData)
 | 
			
		||||
			(target) => isListing(target, isLoginData),
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (!listing) {
 | 
			
		||||
@ -35,14 +33,7 @@ export const POST: RequestHandler = async ({ locals }): Promise<Response> => {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (await compare(password, listing.data.password)) {
 | 
			
		||||
			const token = await jwt.sign(
 | 
			
		||||
				{ sub: listing.id, username, role: listing.data.role },
 | 
			
		||||
				JWT_SECRET,
 | 
			
		||||
				{
 | 
			
		||||
					expiresIn: "1d"
 | 
			
		||||
				}
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			const token = await createToken(listing);
 | 
			
		||||
			return singleResponse(token);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,18 +5,16 @@ import {
 | 
			
		||||
	badRequestResponse,
 | 
			
		||||
	forbiddenResponse,
 | 
			
		||||
	serverErrorResponse,
 | 
			
		||||
	singleResponse
 | 
			
		||||
	singleResponse,
 | 
			
		||||
} from "$lib/server/responseBodies";
 | 
			
		||||
import type { RequestHandler } from "@sveltejs/kit";
 | 
			
		||||
 | 
			
		||||
export const POST: RequestHandler = async ({ request }): Promise<Response> => {
 | 
			
		||||
	let body: unknown;
 | 
			
		||||
 | 
			
		||||
	console.log("here");
 | 
			
		||||
	try {
 | 
			
		||||
		body = await request.json();
 | 
			
		||||
	} catch (err) {
 | 
			
		||||
		console.log(err);
 | 
			
		||||
		return badRequestResponse("body is required");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										83
									
								
								src/tests/hooks.server.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/tests/hooks.server.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
import type { Cookies, RequestEvent } from "@sveltejs/kit";
 | 
			
		||||
import { describe, it, expect, afterEach } from "vitest";
 | 
			
		||||
import * as auth from "../lib/server/auth";
 | 
			
		||||
import { handle } from "../hooks.server";
 | 
			
		||||
import { createId } from "$lib/Id";
 | 
			
		||||
 | 
			
		||||
let events: RequestEvent[] = [];
 | 
			
		||||
 | 
			
		||||
// Mock RequestEvent data that can be passed into the handle function. It doesn't matter
 | 
			
		||||
// that most of these fields are incoherent, the only things that will be tested here are
 | 
			
		||||
// that this event is passed to the isAuthorized function, and that the locas are
 | 
			
		||||
// populated.
 | 
			
		||||
const event: RequestEvent = {
 | 
			
		||||
	cookies: {} as Cookies,
 | 
			
		||||
	fetch: async (): Promise<Response> => {
 | 
			
		||||
		return new Response();
 | 
			
		||||
	},
 | 
			
		||||
	getClientAddress: () => "",
 | 
			
		||||
	locals: { user: {} },
 | 
			
		||||
	params: {},
 | 
			
		||||
	platform: undefined,
 | 
			
		||||
	request: new Request(new URL("https://localhost/api")),
 | 
			
		||||
	route: { id: "" },
 | 
			
		||||
	setHeaders: () => {},
 | 
			
		||||
	url: new URL("https://localhost/api"),
 | 
			
		||||
	isDataRequest: false,
 | 
			
		||||
	isSubRequest: false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const resolve = async (event: RequestEvent): Promise<Response> => {
 | 
			
		||||
	events.push(event);
 | 
			
		||||
	return new Response();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe("handle", () => {
 | 
			
		||||
	afterEach(() => {
 | 
			
		||||
		event.locals.user = {};
 | 
			
		||||
		event.request.headers.delete("authorization");
 | 
			
		||||
		events = [];
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it("returns unauthorized response if caller isn't properly authenticated", async () => {
 | 
			
		||||
		event.request.headers.set("authorization", "Nonesense Token");
 | 
			
		||||
		const res = await handle({ event, resolve });
 | 
			
		||||
		expect(res.status).to.equal(401);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it("returns unauthorized response if caller is missing required auth header", async () => {
 | 
			
		||||
		const res = await handle({ event, resolve });
 | 
			
		||||
		expect(res.status).to.equal(401);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it("returns forbidden response if caller isn't authorized", async () => {
 | 
			
		||||
		// This is a weird scenario, but it does reflect the way I expect this to work.
 | 
			
		||||
		// Svelte Kit does not seem to provide me with a tool that I can use to authorize
 | 
			
		||||
		// users in one place. I would have to check their role at the start of each
 | 
			
		||||
		// endpoint function (yuck). The endpoint below doesn't exist, but it will still
 | 
			
		||||
		// return 403 instead of 404 because the auth check happens before the route
 | 
			
		||||
		// matching, and the user isn't authorized to hit this nonesense endpoint.
 | 
			
		||||
		const ev: RequestEvent = {
 | 
			
		||||
			...event,
 | 
			
		||||
			url: new URL("https://localhost/api/some/secret/route"),
 | 
			
		||||
			request: new Request("https://localhost/api/some/secret/route"),
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const token = await auth.createToken({
 | 
			
		||||
			id: createId(),
 | 
			
		||||
			createdAt: new Date().toString(),
 | 
			
		||||
			modifiedAt: new Date().toString(),
 | 
			
		||||
			deleted: false,
 | 
			
		||||
			data: {
 | 
			
		||||
				password: "somethin' secret!",
 | 
			
		||||
				username: "Mr. Man",
 | 
			
		||||
				role: "default",
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		ev.request.headers.set("authorization", `Bearer ${token}`);
 | 
			
		||||
 | 
			
		||||
		const res = await handle({ event: ev, resolve });
 | 
			
		||||
		expect(res.status).to.equal(403);
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										65
									
								
								src/tests/requests.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/tests/requests.http
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
			
		||||
@token=token
 | 
			
		||||
 | 
			
		||||
GET https://localhost:5173/api
 | 
			
		||||
Accept: application/json
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
 | 
			
		||||
GET https://localhost:5173/api/games
 | 
			
		||||
Accept: application/json
 | 
			
		||||
Authorization: Bearer {{token}}
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
 | 
			
		||||
POST https://localhost:5173/api/games
 | 
			
		||||
Accept: application/json
 | 
			
		||||
Authorization: Bearer {{token}}
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
 | 
			
		||||
GET https://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3
 | 
			
		||||
Accept: application/json
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
 | 
			
		||||
PUT https://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3
 | 
			
		||||
Accept: application/json
 | 
			
		||||
Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
    "state": {},
 | 
			
		||||
    "isStarted": true,
 | 
			
		||||
    "players": ["2", "45", "10"]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
    
 | 
			
		||||
POST https://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3/turns
 | 
			
		||||
Accept: application/json
 | 
			
		||||
Content-Type: application/json
 | 
			
		||||
Authorization: Bearer {{token}}
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
    "kind": "Roll",
 | 
			
		||||
    "player": 2,
 | 
			
		||||
    "value": 4
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
 | 
			
		||||
POST https://localhost:5173/api/users
 | 
			
		||||
Accept: application/json
 | 
			
		||||
Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
    "username": "worf",
 | 
			
		||||
    "password": "klingon",
 | 
			
		||||
    "role": "player"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
 | 
			
		||||
POST https://localhost:5173/api/token
 | 
			
		||||
Accept: application/json
 | 
			
		||||
Content-Type: application/json
 | 
			
		||||
Authorization: Basic worf:klingon
 | 
			
		||||
		Reference in New Issue
	
	Block a user