+ Dev Login (local only)
+
+ This login bypasses OAuth and is only available when running locally
+ (PUBLIC_HOST=localhost). The user is automatically promoted to admin.
+
+
+
+ );
+}
diff --git a/tests/auth.test.ts b/tests/auth.test.ts
index 48f8ba7..c33ff0f 100644
--- a/tests/auth.test.ts
+++ b/tests/auth.test.ts
@@ -25,17 +25,24 @@ describe('session cookie policy', () => {
it('uses the configured cookie domain when one is supplied', () => {
expect(getSessionCookieAttributes('https://my.heygo.cc/app', '.heygo.cc')).toEqual([
'HttpOnly',
- 'Secure',
'SameSite=Lax',
'Path=/',
+ 'Secure',
'Domain=.heygo.cc',
]);
});
+ it('omits Secure over HTTP (local dev) even when a domain is set', () => {
+ expect(getSessionCookieAttributes('http://localhost:8787/app', '')).toEqual([
+ 'HttpOnly',
+ 'SameSite=Lax',
+ 'Path=/',
+ ]);
+ });
+
it('omits Domain when the cookie domain env var is empty (localhost development)', () => {
expect(getSessionCookieAttributes('http://localhost:5173/app', '')).toEqual([
'HttpOnly',
- 'Secure',
'SameSite=Lax',
'Path=/',
]);
@@ -44,9 +51,9 @@ describe('session cookie policy', () => {
it('honors a custom cookie domain for non-heygo hosts', () => {
expect(getSessionCookieAttributes('https://my.example.com/app', '.example.com')).toEqual([
'HttpOnly',
- 'Secure',
'SameSite=Lax',
'Path=/',
+ 'Secure',
'Domain=.example.com',
]);
});
diff --git a/tests/dev-auth.test.ts b/tests/dev-auth.test.ts
new file mode 100644
index 0000000..bf606f7
--- /dev/null
+++ b/tests/dev-auth.test.ts
@@ -0,0 +1,424 @@
+import { describe, expect, it } from 'vitest';
+import worker from '../worker/index';
+import { AUTH_SESSION_COOKIE_NAME, hashSessionToken } from '../worker/auth';
+import type { Env } from '../worker/env';
+
+type UserRole = 'user' | 'admin';
+
+type UserRow = {
+ id: string;
+ email: string;
+ name: string | null;
+ role: UserRole;
+};
+
+type SessionRow = {
+ id: string;
+ user_id: string;
+ session_token_hash: string;
+ expires_at: string;
+};
+
+type RunCall = {
+ sql: string;
+ params: unknown[];
+};
+
+type FirstCall = {
+ sql: string;
+ params: unknown[];
+};
+
+class FakeDevAuthD1 {
+ readonly users: UserRow[] = [];
+ readonly sessions: SessionRow[] = [];
+ readonly runCalls: RunCall[] = [];
+ readonly firstCalls: FirstCall[] = [];
+
+ prepare(sql: string): FakeDevAuthStatement {
+ return new FakeDevAuthStatement(this, sql);
+ }
+
+ findUserByEmail(email: string): UserRow | null {
+ return this.users.find((u) => u.email === email) ?? null;
+ }
+
+ findSessionByHash(hash: string): SessionRow | null {
+ return this.sessions.find((s) => s.session_token_hash === hash) ?? null;
+ }
+}
+
+class FakeDevAuthStatement {
+ private params: unknown[] = [];
+
+ constructor(
+ private readonly db: FakeDevAuthD1,
+ private readonly sql: string,
+ ) {}
+
+ bind(...params: unknown[]): this {
+ this.params = params;
+ return this;
+ }
+
+ async first