From 4bd48253cfca38a3cae63250b1723165461e77e7 Mon Sep 17 00:00:00 2001 From: Erza Bytyci Date: Mon, 25 May 2026 12:11:05 +0000 Subject: [PATCH] fixed 2 bugs that was left for the new team --- gradesa/src/app/admin/users/page.js | 39 +++--- .../api/exercises/dragdrop/[dnd_id]/route.js | 5 +- .../freeform/answers/answer-route.test.js | 1 + .../freeform/answers/response-shape.test.js | 66 ++++++++++ .../api/exercises/freeform/answers/route.js | 2 - .../exercises/jumbled-sentence/[id]/route.js | 123 +++++++++--------- .../api/exercises/memory-game/[id]/route.js | 5 +- .../multichoice/[id]/answers/route.js | 81 ++++++++++++ .../api/exercises/multichoice/[id]/route.js | 16 ++- .../app/api/exercises/multichoice/route.js | 8 +- .../api/exercises/public-route-auth.test.js | 38 ++++++ .../ui/multichoice/multichoicepage.js | 90 ++++++++----- gradesa/src/context/user.context.js | 6 +- gradesa/src/middleware.js | 2 +- gradesa/src/middleware.test.js | 40 ++++++ to-new-team.md | 2 - 16 files changed, 397 insertions(+), 127 deletions(-) create mode 100644 gradesa/src/app/api/exercises/freeform/answers/response-shape.test.js create mode 100644 gradesa/src/app/api/exercises/multichoice/[id]/answers/route.js create mode 100644 gradesa/src/app/api/exercises/public-route-auth.test.js create mode 100644 gradesa/src/middleware.test.js diff --git a/gradesa/src/app/admin/users/page.js b/gradesa/src/app/admin/users/page.js index 63b34af..d581cad 100644 --- a/gradesa/src/app/admin/users/page.js +++ b/gradesa/src/app/admin/users/page.js @@ -3,10 +3,9 @@ import { Column, Row } from "@/components/ui/layout/container"; import useQuery from "@/shared/hooks/useQuery"; import { useRequest } from "@/shared/hooks/useRequest"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useMemo } from "react"; import { Button } from "@/components/ui/button"; -import { useIsSuperAdmin, useUser } from "@/context/user.context"; -import { useRouter } from "next/navigation"; +import { useUser } from "@/context/user.context"; /** * UsersPage @@ -19,30 +18,24 @@ import { useRouter } from "next/navigation"; * - only superadmins may access it */ export default function UsersPage() { - const router = useRouter(); const { auth } = useUser(); - const isSuperAdmin = useIsSuperAdmin(); + const canViewUsers = + auth.isAuthResolved && auth.isLoggedIn && auth.user?.is_superadmin; - const { data: users, error, isLoading, refetch } = useQuery("/admin/users"); + const { + data: users, + error, + isLoading, + refetch, + } = useQuery("/admin/users", undefined, { + enabled: canViewUsers, + }); const makeRequest = useRequest(); const [updatingId, setUpdatingId] = useState(null); const [deletingId, setDeletingId] = useState(null); const [search, setSearch] = useState(""); - /** - * Redirect unauthorized users away from this page. - * - * The `useIsSuperAdmin` hook already performs route protection logic, - * but this extra redirect keeps the page behavior explicit and prevents - * the user from remaining on `/admin/users` after losing access. - */ - useEffect(() => { - if (isSuperAdmin === false) { - router.replace("/"); - } - }, [isSuperAdmin, router]); - /** * Filter users by username or email. * @@ -62,11 +55,15 @@ export default function UsersPage() { }); }, [users, search]); - // Wait until the superadmin check has resolved before rendering the page. - if (isSuperAdmin === undefined) { + // Wait until the auth check has resolved before rendering the page. + if (!auth.isAuthResolved) { return

Wird geladen...

; } + if (!auth.isLoggedIn || !auth.user?.is_superadmin) { + return

Sie haben keinen Zugriff auf diese Seite.

; + } + if (isLoading) { return

Wird geladen...

; } diff --git a/gradesa/src/app/api/exercises/dragdrop/[dnd_id]/route.js b/gradesa/src/app/api/exercises/dragdrop/[dnd_id]/route.js index 62e557d..3388d0a 100644 --- a/gradesa/src/app/api/exercises/dragdrop/[dnd_id]/route.js +++ b/gradesa/src/app/api/exercises/dragdrop/[dnd_id]/route.js @@ -1,6 +1,7 @@ import { DB } from "@/backend/db"; +import { withAuth } from "@/backend/middleware/withAuth"; -export async function GET(request, { params }) { +export const GET = withAuth(async (request, { params }) => { const { dnd_id } = await params; if (!dnd_id || isNaN(parseInt(dnd_id, 10))) { @@ -64,4 +65,4 @@ export async function GET(request, { params }) { { status: 500 } ); } -} +}); diff --git a/gradesa/src/app/api/exercises/freeform/answers/answer-route.test.js b/gradesa/src/app/api/exercises/freeform/answers/answer-route.test.js index a6b82a0..938cdcf 100644 --- a/gradesa/src/app/api/exercises/freeform/answers/answer-route.test.js +++ b/gradesa/src/app/api/exercises/freeform/answers/answer-route.test.js @@ -80,6 +80,7 @@ describe("freeform answers", () => { expect(result.similarity).toBe(1); expect(result.perfectAnswer).toBe(true); expect(result.feedback).toBe("Great job!"); + expect(result.answer).toBeUndefined(); }); it("should evaluate similar but not exact answers", async () => { diff --git a/gradesa/src/app/api/exercises/freeform/answers/response-shape.test.js b/gradesa/src/app/api/exercises/freeform/answers/response-shape.test.js new file mode 100644 index 0000000..6cc3946 --- /dev/null +++ b/gradesa/src/app/api/exercises/freeform/answers/response-shape.test.js @@ -0,0 +1,66 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { POST } from "./route"; + +vi.mock("@/backend/auth/session", () => ({ + checkSession: vi.fn(), +})); + +vi.mock("@/backend/db", () => ({ + DB: { + pool: vi.fn(), + }, +})); + +import { checkSession } from "@/backend/auth/session"; +import { DB } from "@/backend/db"; + +function createRequest(body) { + return { + url: "http://localhost/api/exercises/freeform/answers", + headers: new Headers({ "content-type": "application/json" }), + method: "POST", + cache: "default", + credentials: "same-origin", + integrity: "", + mode: "cors", + redirect: "follow", + json: async () => body, + }; +} + +describe("freeform answer response shape", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("does not echo the canonical answer", async () => { + checkSession.mockResolvedValue({ id: 7, is_admin: false }); + DB.pool + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + answer: "This is the correct answer", + is_correct: true, + feedback: "Great job!", + }, + ], + }) + .mockResolvedValueOnce({ rows: [] }); + + const response = await POST( + createRequest({ + freeFormExerciseId: "12", + freeFormQuestionId: "34", + answer: "This is the correct answer", + }) + ); + + expect(response.status).toBe(200); + + const result = await response.json(); + expect(result.answer).toBeUndefined(); + expect(result.is_correct).toBe(true); + expect(result.perfectAnswer).toBe(true); + }); +}); diff --git a/gradesa/src/app/api/exercises/freeform/answers/route.js b/gradesa/src/app/api/exercises/freeform/answers/route.js index 1ca5d53..d27d96b 100644 --- a/gradesa/src/app/api/exercises/freeform/answers/route.js +++ b/gradesa/src/app/api/exercises/freeform/answers/route.js @@ -260,7 +260,6 @@ export const POST = withAuth( return NextResponse.json({ is_correct: false, - answer: "", feedback: "Your answer doesn't match any of the expected answers. Please try again.", similarity: bestMatch.similarity, @@ -307,7 +306,6 @@ export const POST = withAuth( return NextResponse.json({ is_correct: isAnswerCorrect, - answer: bestMatch.answer, similarity: bestMatch.similarity, perfectAnswer, comparisonDetails, diff --git a/gradesa/src/app/api/exercises/jumbled-sentence/[id]/route.js b/gradesa/src/app/api/exercises/jumbled-sentence/[id]/route.js index 819d41a..848c558 100644 --- a/gradesa/src/app/api/exercises/jumbled-sentence/[id]/route.js +++ b/gradesa/src/app/api/exercises/jumbled-sentence/[id]/route.js @@ -1,8 +1,9 @@ import { NextResponse } from "next/server"; import { jumbledSentenceExerciseSchema } from "@/shared/schemas/jumbled-sentence.schemas"; import { DB } from "@/backend/db"; +import { withAuth } from "@/backend/middleware/withAuth"; -export async function GET(request, { params }) { +export const GET = withAuth(async (request, { params }) => { const { id } = await params; try { const exRes = await DB.pool( @@ -29,65 +30,71 @@ export async function GET(request, { params }) { } catch (err) { return NextResponse.json({ error: err.message }, { status: 500 }); } -} +}); -export async function PUT(request, { params }) { - const { id } = await params; - const body = await request.json(); - const parseResult = jumbledSentenceExerciseSchema.safeParse(body); - if (!parseResult.success) { - return NextResponse.json( - { error: parseResult.error.errors }, - { status: 400 } - ); - } - const { title, sentences } = body; - try { - await DB.transaction(async (client) => { - await client.query( - `UPDATE jumbled_sentence_exercises SET title = $1, updated_at = NOW() WHERE id = $2`, - [title, id] +export const PUT = withAuth( + async (request, { params }) => { + const { id } = await params; + const body = await request.json(); + const parseResult = jumbledSentenceExerciseSchema.safeParse(body); + if (!parseResult.success) { + return NextResponse.json( + { error: parseResult.error.errors }, + { status: 400 } ); - await client.query( - `DELETE FROM jumbled_sentence_sentences WHERE jumbled_exercise_id = $1`, - [id] - ); - for (const s of sentences) { + } + const { title, sentences } = body; + try { + await DB.transaction(async (client) => { await client.query( - `INSERT INTO jumbled_sentence_sentences (jumbled_exercise_id, sentence, alternates, correct_feedback, alternate_feedbacks, incorrect_feedbacks, incorrect_alternates) VALUES ($1, $2, $3, $4, $5, $6, $7)`, - [ - id, - s.sentence, - s.alternates?.filter(Boolean) || [], - s.correctSentenceFeedback || null, - s.alternateFeedbacks || [], - s.incorrectFeedbacks || [], - s.incorrectAlternates || [], - ] + `UPDATE jumbled_sentence_exercises SET title = $1, updated_at = NOW() WHERE id = $2`, + [title, id] ); - } - }); - return NextResponse.json({ success: true }); - } catch (err) { - return NextResponse.json({ error: err.message }, { status: 500 }); - } -} + await client.query( + `DELETE FROM jumbled_sentence_sentences WHERE jumbled_exercise_id = $1`, + [id] + ); + for (const s of sentences) { + await client.query( + `INSERT INTO jumbled_sentence_sentences (jumbled_exercise_id, sentence, alternates, correct_feedback, alternate_feedbacks, incorrect_feedbacks, incorrect_alternates) VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [ + id, + s.sentence, + s.alternates?.filter(Boolean) || [], + s.correctSentenceFeedback || null, + s.alternateFeedbacks || [], + s.incorrectFeedbacks || [], + s.incorrectAlternates || [], + ] + ); + } + }); + return NextResponse.json({ success: true }); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } + }, + { requireAdmin: true } +); -export async function DELETE(request, { params }) { - const { id } = await params; - try { - await DB.transaction(async (client) => { - await client.query( - `DELETE FROM jumbled_sentence_sentences WHERE jumbled_exercise_id = $1`, - [id] - ); - await client.query( - `DELETE FROM jumbled_sentence_exercises WHERE id = $1`, - [id] - ); - }); - return NextResponse.json({ success: true }); - } catch (err) { - return NextResponse.json({ error: err.message }, { status: 500 }); - } -} +export const DELETE = withAuth( + async (request, { params }) => { + const { id } = await params; + try { + await DB.transaction(async (client) => { + await client.query( + `DELETE FROM jumbled_sentence_sentences WHERE jumbled_exercise_id = $1`, + [id] + ); + await client.query( + `DELETE FROM jumbled_sentence_exercises WHERE id = $1`, + [id] + ); + }); + return NextResponse.json({ success: true }); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } + }, + { requireAdmin: true } +); diff --git a/gradesa/src/app/api/exercises/memory-game/[id]/route.js b/gradesa/src/app/api/exercises/memory-game/[id]/route.js index f077d36..2a11bac 100644 --- a/gradesa/src/app/api/exercises/memory-game/[id]/route.js +++ b/gradesa/src/app/api/exercises/memory-game/[id]/route.js @@ -1,7 +1,8 @@ import { NextResponse } from "next/server"; import { DB } from "@/backend/db"; +import { withAuth } from "@/backend/middleware/withAuth"; -export async function GET(request, { params }) { +export const GET = withAuth(async (request, { params }) => { const { id } = await params; if (!id || Number.isNaN(Number(id))) { @@ -48,4 +49,4 @@ export async function GET(request, { params }) { { status: 500 } ); } -} +}); diff --git a/gradesa/src/app/api/exercises/multichoice/[id]/answers/route.js b/gradesa/src/app/api/exercises/multichoice/[id]/answers/route.js new file mode 100644 index 0000000..8dbc2e0 --- /dev/null +++ b/gradesa/src/app/api/exercises/multichoice/[id]/answers/route.js @@ -0,0 +1,81 @@ +import { NextResponse } from "next/server"; +import { DB } from "@/backend/db"; +import { withAuth } from "@/backend/middleware/withAuth"; + +export const POST = withAuth(async (request, { params }) => { + const { id } = await params; + const body = await request.json(); + const answers = Array.isArray(body?.answers) ? body.answers : []; + + if (!id || Number.isNaN(Number(id))) { + return NextResponse.json( + { error: "Invalid exercise ID." }, + { status: 400 } + ); + } + + if (answers.length === 0) { + return NextResponse.json( + { error: "Answers are required." }, + { status: 422 } + ); + } + + try { + const { rows: contentRows } = await DB.pool( + `SELECT id, content_type, correct_answer + FROM multichoice_content + WHERE multichoice_exercise_id = $1 + ORDER BY content_order`, + [id] + ); + + if (contentRows.length === 0) { + return NextResponse.json( + { error: "Exercise not found." }, + { status: 404 } + ); + } + + const answerMap = new Map( + answers + .filter((entry) => entry && entry.contentId != null) + .map((entry) => [String(entry.contentId), String(entry.answer || "")]) + ); + + const results = contentRows + .filter( + (item) => + item.content_type === "multichoice" || item.content_type === "gap" + ) + .map((item) => { + const submitted = (answerMap.get(String(item.id)) || "").trim(); + const expected = String(item.correct_answer || "").trim(); + const isCorrect = + submitted.length > 0 && + submitted.toLowerCase() === expected.toLowerCase(); + + return { + contentId: item.id, + isCorrect, + answered: submitted.length > 0, + }; + }); + + const hasMissing = results.some((entry) => !entry.answered); + const allCorrect = + results.length > 0 && results.every((entry) => entry.isCorrect); + + return NextResponse.json({ + results, + hasMissing, + allCorrect, + }); + } catch (error) { + console.error("Error validating multichoice answers:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +}); diff --git a/gradesa/src/app/api/exercises/multichoice/[id]/route.js b/gradesa/src/app/api/exercises/multichoice/[id]/route.js index ea8c02d..da9fec0 100644 --- a/gradesa/src/app/api/exercises/multichoice/[id]/route.js +++ b/gradesa/src/app/api/exercises/multichoice/[id]/route.js @@ -1,7 +1,7 @@ import { DB } from "@/backend/db"; +import { withAuth } from "@/backend/middleware/withAuth"; -export async function GET(request, { params }) { - // --- FIX IS HERE --- +export const GET = withAuth(async (request, { params }) => { // In Next.js 15, params is a Promise, so you must await it. const { id } = await params; @@ -23,7 +23,15 @@ export async function GET(request, { params }) { // Fetch exercise content const contentResult = await DB.pool( - "SELECT * FROM multichoice_content WHERE multichoice_exercise_id = $1 ORDER BY content_order", + `SELECT + id, + multichoice_exercise_id, + content_type, + content_value, + content_order + FROM multichoice_content + WHERE multichoice_exercise_id = $1 + ORDER BY content_order`, [id] ); const content = contentResult.rows; @@ -61,4 +69,4 @@ export async function GET(request, { params }) { { status: 500 } ); } -} +}); diff --git a/gradesa/src/app/api/exercises/multichoice/route.js b/gradesa/src/app/api/exercises/multichoice/route.js index 65ec199..23fe211 100644 --- a/gradesa/src/app/api/exercises/multichoice/route.js +++ b/gradesa/src/app/api/exercises/multichoice/route.js @@ -1,7 +1,8 @@ import { NextResponse } from "next/server"; import { DB } from "@/backend/db"; +import { withAuth } from "@/backend/middleware/withAuth"; -export async function GET() { +export const GET = withAuth(async () => { try { // Fetch multichoice exercises const { rows: exercises } = await DB.pool(` @@ -30,8 +31,7 @@ export async function GET() { mc.multichoice_exercise_id, mc.content_type, mc.content_value, - mc.content_order, - mc.correct_answer + mc.content_order FROM multichoice_content mc WHERE @@ -83,4 +83,4 @@ export async function GET() { { status: 500 } ); } -} +}); diff --git a/gradesa/src/app/api/exercises/public-route-auth.test.js b/gradesa/src/app/api/exercises/public-route-auth.test.js new file mode 100644 index 0000000..07db8ac --- /dev/null +++ b/gradesa/src/app/api/exercises/public-route-auth.test.js @@ -0,0 +1,38 @@ +import { describe, it, expect, vi } from "vitest"; +import { checkSession } from "@/backend/auth/session"; +import { GET as getClick } from "./click/route"; +import { GET as getDragdrop } from "./dragdrop/[dnd_id]/route"; +import { GET as getMultichoiceList } from "./multichoice/route"; +import { GET as getMultichoiceDetail } from "./multichoice/[id]/route"; +import { GET as getMemoryGame } from "./memory-game/[id]/route"; +import { + GET as getJumbledSentence, + PUT as putJumbledSentence, + DELETE as deleteJumbledSentence, +} from "./jumbled-sentence/[id]/route"; + +vi.mock("@/backend/auth/session", () => ({ + checkSession: vi.fn(), +})); + +describe("exercise API auth guards", () => { + it("rejects unauthenticated access to answer-bearing routes", async () => { + checkSession.mockResolvedValue(null); + + const cases = [ + [getClick, {}], + [getDragdrop, { params: Promise.resolve({ dnd_id: "1" }) }], + [getMultichoiceList, {}], + [getMultichoiceDetail, { params: Promise.resolve({ id: "1" }) }], + [getMemoryGame, { params: Promise.resolve({ id: "1" }) }], + [getJumbledSentence, { params: Promise.resolve({ id: "1" }) }], + [putJumbledSentence, { params: Promise.resolve({ id: "1" }) }], + [deleteJumbledSentence, { params: Promise.resolve({ id: "1" }) }], + ]; + + for (const [handler, context] of cases) { + const response = await handler({}, context); + expect(response.status).toBe(401); + } + }); +}); diff --git a/gradesa/src/components/ui/multichoice/multichoicepage.js b/gradesa/src/components/ui/multichoice/multichoicepage.js index 6896fbb..fed183e 100644 --- a/gradesa/src/components/ui/multichoice/multichoicepage.js +++ b/gradesa/src/components/ui/multichoice/multichoicepage.js @@ -6,6 +6,7 @@ import { Row } from "../layout/container"; import "./multichoice.css"; import { Button } from "@/components/ui/button"; import useQuery from "@/shared/hooks/useQuery"; +import { useRequest } from "@/shared/hooks/useRequest"; function shuffleOptions(array) { return array @@ -15,6 +16,7 @@ function shuffleOptions(array) { } export default function MultichoicePage({ exerciseId }) { + const makeRequest = useRequest(); // Fetch exercise data using the useQuery hook const { data: exerciseData, @@ -75,37 +77,65 @@ export default function MultichoicePage({ exerciseId }) { }; const handleSubmit = () => { - const hasMissingAnswers = userAnswers.some((answer) => answer === ""); - const newCheckedAnswers = shuffledExerciseData.content.map( - (item, index) => { - if ( - item.content_type === "multichoice" || - item.content_type === "gap" - ) { - return ( - userAnswers[index]?.trim().toLowerCase() === - item.correct_answer.toLowerCase() - ); - } - return true; + (async () => { + try { + const answerEntries = shuffledExerciseData.content + .map((item, index) => ({ item, index })) + .filter( + ({ item }) => + item.content_type === "multichoice" || item.content_type === "gap" + ) + .map(({ item, index }) => ({ + contentId: item.id, + answer: userAnswers[index] || "", + })); + + const response = await makeRequest( + `/exercises/multichoice/${exerciseId}/answers`, + { + answers: answerEntries, + } + ); + + const resultByContentId = new Map( + (response.data?.results || []).map((entry) => [ + String(entry.contentId), + entry, + ]) + ); + + const newCheckedAnswers = shuffledExerciseData.content.map((item) => { + if ( + item.content_type === "multichoice" || + item.content_type === "gap" + ) { + return !!resultByContentId.get(String(item.id))?.isCorrect; + } + return true; + }); + + const hasMissingAnswers = !!response.data?.hasMissing; + const hasIncorrectAnswers = newCheckedAnswers.some( + (isCorrect, index) => { + const contentType = + shuffledExerciseData.content[index].content_type; + const isAnswerField = + contentType === "multichoice" || contentType === "gap"; + + return isAnswerField && !isCorrect; + } + ); + + setHasErrors(hasMissingAnswers || hasIncorrectAnswers); + setCheckedAnswers(newCheckedAnswers); + setIsSubmitted(true); + setDropdownSubmittedStates(newCheckedAnswers.map(() => true)); + } catch (submitError) { + console.error("Error validating multichoice answers:", submitError); + setHasErrors(true); + setIsSubmitted(true); } - ); - - const hasIncorrectAnswers = newCheckedAnswers.some((isCorrect, index) => { - const contentType = shuffledExerciseData.content[index].content_type; - const isAnswerField = - contentType === "multichoice" || contentType === "gap"; - - return isAnswerField && !isCorrect; - }); - - // Set error state if there are missing or incorrect answers - setHasErrors(hasMissingAnswers || hasIncorrectAnswers); - setCheckedAnswers(newCheckedAnswers); - setIsSubmitted(true); - - // Mark all dropdowns as submitted - setDropdownSubmittedStates(newCheckedAnswers.map(() => true)); + })(); }; const handleReset = () => { diff --git a/gradesa/src/context/user.context.js b/gradesa/src/context/user.context.js index 193613f..6ec0f6e 100644 --- a/gradesa/src/context/user.context.js +++ b/gradesa/src/context/user.context.js @@ -183,6 +183,8 @@ export function useIsAdmin() { const { auth, actAs } = useUser(); useEffect(() => { + if (!auth.isAuthResolved) return; + if (!auth.user?.id) return; const isAllowedAdminView = @@ -194,7 +196,9 @@ export function useIsAdmin() { } }, [auth, router, pathname]); - return auth?.user?.id ? auth.user.is_admin : undefined; + if (!auth.isAuthResolved) return undefined; + + return auth.isLoggedIn && !!auth.user?.is_admin; } /** diff --git a/gradesa/src/middleware.js b/gradesa/src/middleware.js index 2cf72ec..ac26da4 100644 --- a/gradesa/src/middleware.js +++ b/gradesa/src/middleware.js @@ -11,7 +11,7 @@ import { AUTH_COOKIE_NAME } from "@/shared/const"; const authRequired = ["/grammar/exercises", "/edit_info", "/contact"]; // Protected paths that should send logged-out users to registration first. -const registerFirstPaths = ["/contact"]; +const registerFirstPaths = ["/contact", "/grammar/exercises"]; // Restricted paths logged-in users cannot access /** diff --git a/gradesa/src/middleware.test.js b/gradesa/src/middleware.test.js new file mode 100644 index 0000000..e7e6cbf --- /dev/null +++ b/gradesa/src/middleware.test.js @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { middleware } from "./middleware"; + +function createRequest(pathname, hasCookie = false) { + return { + url: `http://localhost/gradesa${pathname}`, + nextUrl: { + pathname: `/gradesa${pathname}`, + }, + cookies: { + get: () => (hasCookie ? { value: "session-cookie" } : undefined), + }, + }; +} + +describe("middleware", () => { + it("redirects logged-out users from exercises to registration", async () => { + const response = await middleware(createRequest("/grammar/exercises")); + + expect(response.status).toBe(307); + expect(response.headers.get("location")).toContain("/auth/register"); + expect(response.headers.get("location")).toContain( + "redirect=%2Fgrammar%2Fexercises" + ); + }); + + it("redirects logged-out users from contact to registration", async () => { + const response = await middleware(createRequest("/contact")); + + expect(response.status).toBe(307); + expect(response.headers.get("location")).toContain("/auth/register"); + }); + + it("redirects other protected paths to login", async () => { + const response = await middleware(createRequest("/edit_info")); + + expect(response.status).toBe(307); + expect(response.headers.get("location")).toContain("/auth/login"); + }); +}); diff --git a/to-new-team.md b/to-new-team.md index 10b8be4..b3d7897 100644 --- a/to-new-team.md +++ b/to-new-team.md @@ -18,8 +18,6 @@ For the future: - The following things could be improved in the GitHub Actions pipeline: - Make the tests faster by for example making e2e-tests run at the same time as other tests. - Modify the tests so that the e2e-tests test the built container, not just the code. This way the tests catch if there are problems with for example dependencies marked only as dev-dependencies (since they are removed when the image is built). -- Fix this bug: If you go to the front page and select 'Übungen' from the 'Grammatik' box while you are not logged in you are brought to the login page. If you input valid credentials there and try to log in, you will see that nothing happens. -- Fix this: If you want to see the correct answers to some exercises, you can get the correct answers straight from the API by for example opening https://gradesa-ohtuprojekti-staging.ext.ocp-prod-0.k8s.it.helsinki.fi/api/exercises/dragdrop/1 in your browser. You do not even have to be logged in. !!! ADHERE TO THE DEFINITION OF DONE !!!