feat: guest play — allow singleplayer quiz without auth
- Add optionalAuth middleware: attaches session when present, never blocks (guests pass through) - Make game endpoints (start/answer) accept optional auth - GameSessionStore.userId: string → string | null - Rate limiter falls back to IP for unauthenticated users - Frontend: remove /play route guard, show 'Create account' CTA on score screen for guests - Add tests for guest session creation, answer submission, and cross-user session isolation
This commit is contained in:
parent
d55a1ed648
commit
0118798e36
11 changed files with 298 additions and 32 deletions
|
|
@ -332,3 +332,89 @@ describe("evaluateAnswer", () => {
|
|||
).rejects.toMatchObject({ statusCode: 422 });
|
||||
});
|
||||
});
|
||||
|
||||
// Add to existing gameService.test.ts
|
||||
|
||||
describe("createGameSession — guest", () => {
|
||||
let store: InMemoryGameSessionStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new InMemoryGameSessionStore();
|
||||
});
|
||||
|
||||
it("creates a session with userId null for guests", async () => {
|
||||
const session = await createGameSession(validRequest, store, null);
|
||||
|
||||
expect(session.sessionId).toBeDefined();
|
||||
expect(session.questions).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("stores userId as null in the session store", async () => {
|
||||
const session = await createGameSession(validRequest, store, null);
|
||||
const stored = await store.get(session.sessionId);
|
||||
|
||||
expect(stored).not.toBeNull();
|
||||
expect(stored!.userId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("evaluateAnswer — guest", () => {
|
||||
let store: InMemoryGameSessionStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new InMemoryGameSessionStore();
|
||||
});
|
||||
|
||||
it("allows a guest to answer their own session", async () => {
|
||||
const session = await createGameSession(validRequest, store, null);
|
||||
const question = session.questions[0]!;
|
||||
const correctText = fakeTerms[0]!.targetText;
|
||||
const correctOption = question.options.find((o) => o.text === correctText)!;
|
||||
|
||||
const result = await evaluateAnswer(
|
||||
{
|
||||
sessionId: session.sessionId,
|
||||
questionId: question.questionId,
|
||||
selectedOptionId: correctOption.optionId,
|
||||
},
|
||||
store,
|
||||
null,
|
||||
);
|
||||
|
||||
expect(result.isCorrect).toBe(true);
|
||||
});
|
||||
|
||||
it("throws NotFoundError when guest tries to answer an authenticated session", async () => {
|
||||
const authSession = await createGameSession(validRequest, store, "user-1");
|
||||
const question = authSession.questions[0]!;
|
||||
|
||||
await expect(
|
||||
evaluateAnswer(
|
||||
{
|
||||
sessionId: authSession.sessionId,
|
||||
questionId: question.questionId,
|
||||
selectedOptionId: 0,
|
||||
},
|
||||
store,
|
||||
null,
|
||||
),
|
||||
).rejects.toThrow("Game session not found");
|
||||
});
|
||||
|
||||
it("throws NotFoundError when authenticated user tries to answer a guest session", async () => {
|
||||
const guestSession = await createGameSession(validRequest, store, null);
|
||||
const question = guestSession.questions[0]!;
|
||||
|
||||
await expect(
|
||||
evaluateAnswer(
|
||||
{
|
||||
sessionId: guestSession.sessionId,
|
||||
questionId: question.questionId,
|
||||
selectedOptionId: 0,
|
||||
},
|
||||
store,
|
||||
"user-1",
|
||||
),
|
||||
).rejects.toThrow("Game session not found");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { shuffleArray } from "../lib/utils.js";
|
|||
export const createGameSession = async (
|
||||
request: GameRequest,
|
||||
store: GameSessionStore,
|
||||
userId: string,
|
||||
userId: string | null,
|
||||
): Promise<GameSession> => {
|
||||
const terms = await getGameTerms(
|
||||
request.source_language,
|
||||
|
|
@ -87,11 +87,15 @@ export const createGameSession = async (
|
|||
export const evaluateAnswer = async (
|
||||
submission: AnswerSubmission,
|
||||
store: GameSessionStore,
|
||||
userId: string,
|
||||
userId: string | null,
|
||||
): Promise<AnswerResult> => {
|
||||
const session = await store.get(submission.sessionId);
|
||||
|
||||
if (!session || session.userId !== userId) {
|
||||
if (!session) {
|
||||
throw new NotFoundError(`Game session not found: ${submission.sessionId}`);
|
||||
}
|
||||
|
||||
if (session.userId !== userId) {
|
||||
throw new NotFoundError(`Game session not found: ${submission.sessionId}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue