Merge pull request #64 from open-reception/session_fix

Fix session expiration and tests
This commit is contained in:
Karl Ludwig Weise
2025-09-01 15:59:54 +02:00
committed by GitHub
5 changed files with 426 additions and 2 deletions
@@ -0,0 +1,209 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach } from "vitest";
import { SessionService } from "../session-service";
import * as jwtUtils from "../jwt-utils";
// Mock dependencies
vi.mock("$lib/server/db", () => ({
db: {
select: vi.fn(),
update: vi.fn()
}
}));
vi.mock("../jwt-utils");
const mockUser = {
id: "test-user-id",
email: "test@example.com",
name: "Test User",
role: "STAFF" as const,
tenantId: "tenant-123",
isActive: true,
confirmed: true,
createdAt: new Date(),
updatedAt: new Date(),
lastLoginAt: null
};
const mockSession = {
id: "session-id",
userId: "test-user-id",
sessionToken: "session-token",
accessToken: "access-token",
refreshToken: "refresh-token",
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
lastUsedAt: new Date(),
ipAddress: "127.0.0.1",
userAgent: "test-agent",
createdAt: new Date(),
updatedAt: new Date()
};
describe("SessionService.validateTokenWithDB", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should return user, sessionId and exp for valid token", async () => {
const { db } = await import("$lib/server/db");
// Mock JWT verification
const mockTokenData = {
sessionId: "session-id",
userId: "test-user-id",
exp: Math.floor(Date.now() / 1000) + 3600
};
vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
// Mock successful database query
const mockQuery = vi.fn().mockResolvedValue([
{
user: mockUser,
user_session: mockSession
}
]);
vi.mocked(db.select).mockReturnValue({
from: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
limit: mockQuery
})
})
})
} as any);
vi.mocked(db.update).mockReturnValue({
set: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue([])
})
} as any);
const result = await SessionService.validateTokenWithDB("valid-token");
expect(result).toEqual({
user: mockUser,
sessionId: "session-id",
exp: mockSession.expiresAt
});
expect(jwtUtils.verifyAccessToken).toHaveBeenCalledWith("valid-token");
});
it("should return null for invalid JWT token", async () => {
vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(null);
const result = await SessionService.validateTokenWithDB("invalid-token");
expect(result).toBeNull();
expect(jwtUtils.verifyAccessToken).toHaveBeenCalledWith("invalid-token");
});
it("should return null for non-existent session", async () => {
const { db } = await import("$lib/server/db");
const mockTokenData = {
sessionId: "non-existent-session",
userId: "test-user-id",
exp: Math.floor(Date.now() / 1000) + 3600
};
vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
// Mock empty database result
const mockQuery = vi.fn().mockResolvedValue([]);
vi.mocked(db.select).mockReturnValue({
from: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
limit: mockQuery
})
})
})
} as any);
const result = await SessionService.validateTokenWithDB("valid-token");
expect(result).toBeNull();
});
it("should return null for inactive user", async () => {
const { db } = await import("$lib/server/db");
const mockTokenData = {
sessionId: "session-id",
userId: "test-user-id",
exp: Math.floor(Date.now() / 1000) + 3600
};
vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
const inactiveUser = {
...mockUser,
isActive: false
};
const mockQuery = vi.fn().mockResolvedValue([
{
user: inactiveUser,
user_session: mockSession
}
]);
vi.mocked(db.select).mockReturnValue({
from: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
limit: mockQuery
})
})
})
} as any);
const result = await SessionService.validateTokenWithDB("valid-token");
expect(result).toBeNull();
});
it("should return null for unconfirmed user", async () => {
const { db } = await import("$lib/server/db");
const mockTokenData = {
sessionId: "session-id",
userId: "test-user-id",
exp: Math.floor(Date.now() / 1000) + 3600
};
vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
const unconfirmedUser = {
...mockUser,
confirmed: false
};
const mockQuery = vi.fn().mockResolvedValue([
{
user: unconfirmedUser,
user_session: mockSession
}
]);
vi.mocked(db.select).mockReturnValue({
from: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
limit: mockQuery
})
})
})
} as any);
const result = await SessionService.validateTokenWithDB("valid-token");
expect(result).toBeNull();
});
it("should return null and handle errors gracefully", async () => {
vi.mocked(jwtUtils.verifyAccessToken).mockRejectedValue(new Error("JWT error"));
const result = await SessionService.validateTokenWithDB("token");
expect(result).toBeNull();
});
});
+2 -1
View File
@@ -106,7 +106,7 @@ export class SessionService {
*/
static async validateTokenWithDB(
accessToken: string
): Promise<{ user: SelectUser; sessionId: string } | null> {
): Promise<{ user: SelectUser; sessionId: string; exp: Date } | null> {
logger.debug("Validating access token with database");
try {
@@ -158,6 +158,7 @@ export class SessionService {
return {
user: session.user,
exp: session.user_session.expiresAt,
sessionId: session.user_session.id
};
} catch (error) {
+1 -1
View File
@@ -84,7 +84,7 @@ export const GET: RequestHandler = async ({ locals }) => {
role: locals.user.role,
tenantId: locals.user.tenantId
},
expiresAt: new Date(locals.user.exp! * 1000).toISOString()
expiresAt: new Date(locals.user.exp ?? 0).toISOString()
});
} catch (error) {
logger.error("Session check error:", { error: String(error) });
@@ -0,0 +1,213 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi } from "vitest";
import { GET } from "../+server";
import type { RequestEvent } from "@sveltejs/kit";
// Mock the logger
vi.mock("$lib/logger", () => ({
UniversalLogger: vi.fn(() => ({
setContext: vi.fn(() => ({
error: vi.fn(),
debug: vi.fn()
}))
}))
}));
// Mock OpenAPI registration
vi.mock("$lib/server/openapi", () => ({
registerOpenAPIRoute: vi.fn()
}));
const createMockRequestEvent = (locals: any): RequestEvent =>
({
locals,
request: new Request("http://localhost/api/auth/session"),
url: new URL("http://localhost/api/auth/session"),
params: {},
route: { id: "/api/auth/session" },
cookies: {} as any,
fetch: fetch,
getClientAddress: () => "127.0.0.1",
isDataRequest: false,
platform: undefined,
setHeaders: vi.fn(),
depends: vi.fn(),
parent: vi.fn()
}) as any;
describe("/api/auth/session GET endpoint", () => {
it("should return user session data with correct exp field when user is authenticated", async () => {
const mockUser = {
userId: "user-123",
email: "test@example.com",
name: "Test User",
role: "STAFF",
tenantId: "tenant-123",
sessionId: "session-123",
exp: 1640995200000, // timestamp in milliseconds
isActive: true,
confirmed: true
};
const event = createMockRequestEvent({ user: mockUser });
const response = await GET(event as any);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toEqual({
authenticated: true,
user: {
id: "user-123",
email: "test@example.com",
name: "Test User",
role: "STAFF",
tenantId: "tenant-123"
},
expiresAt: new Date(1640995200000).toISOString()
});
});
it("should handle exp as undefined gracefully", async () => {
const mockUser = {
userId: "user-123",
email: "test@example.com",
name: "Test User",
role: "STAFF",
tenantId: "tenant-123",
sessionId: "session-123",
exp: undefined, // Test the null coalescing operator
isActive: true,
confirmed: true
};
const event = createMockRequestEvent({ user: mockUser });
const response = await GET(event as any);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toEqual({
authenticated: true,
user: {
id: "user-123",
email: "test@example.com",
name: "Test User",
role: "STAFF",
tenantId: "tenant-123"
},
expiresAt: new Date(0).toISOString() // Should default to 0
});
});
it("should handle exp as null gracefully", async () => {
const mockUser = {
userId: "user-123",
email: "test@example.com",
name: "Test User",
role: "STAFF",
tenantId: "tenant-123",
sessionId: "session-123",
exp: null, // Test the null coalescing operator
isActive: true,
confirmed: true
};
const event = createMockRequestEvent({ user: mockUser });
const response = await GET(event as any);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toEqual({
authenticated: true,
user: {
id: "user-123",
email: "test@example.com",
name: "Test User",
role: "STAFF",
tenantId: "tenant-123"
},
expiresAt: new Date(0).toISOString() // Should default to 0
});
});
it("should return 401 when user is not authenticated", async () => {
const event = createMockRequestEvent({ user: null });
const response = await GET(event as any);
expect(response.status).toBe(401);
const data = await response.json();
expect(data).toEqual({
authenticated: false,
message: "Not authenticated"
});
});
it("should handle global admin without tenantId", async () => {
const mockGlobalAdmin = {
userId: "admin-123",
email: "admin@example.com",
name: "Global Admin",
role: "GLOBAL_ADMIN",
tenantId: undefined, // Global admin has no tenant
sessionId: "session-123",
exp: 1640995200000,
isActive: true,
confirmed: true
};
const event = createMockRequestEvent({ user: mockGlobalAdmin });
const response = await GET(event as any);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toEqual({
authenticated: true,
user: {
id: "admin-123",
email: "admin@example.com",
name: "Global Admin",
role: "GLOBAL_ADMIN",
tenantId: undefined
},
expiresAt: new Date(1640995200000).toISOString()
});
});
it("should handle exp as 0 (epoch time)", async () => {
const mockUser = {
userId: "user-123",
email: "test@example.com",
name: "Test User",
role: "STAFF",
tenantId: "tenant-123",
sessionId: "session-123",
exp: 0, // Epoch time
isActive: true,
confirmed: true
};
const event = createMockRequestEvent({ user: mockUser });
const response = await GET(event as any);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.expiresAt).toBe(new Date(0).toISOString());
});
it("should return 500 when an error occurs", async () => {
// Create a mock that throws an error when accessing properties
const problematicLocals = {
get user() {
throw new Error("Database connection failed");
}
};
const event = createMockRequestEvent(problematicLocals);
const response = await GET(event as any);
expect(response.status).toBe(500);
const data = await response.json();
expect(data).toEqual({
error: "Internal server error"
});
});
});
+1
View File
@@ -92,6 +92,7 @@ export const apiAuthHandle: Handle = async ({ event, resolve }) => {
// Add sessionId to the user object for easy access
event.locals.user = {
userId: sessionData.user.id,
exp: sessionData.exp.valueOf(),
...sessionData.user,
sessionId: sessionData.sessionId
};