Skip to content

Commit 1e10366

Browse files
fix(exams): pass tenant_id to exam_submissions insert
The exam submission INSERT was missing tenant_id, causing it to use the default tenant UUID from the column default. RLS then blocked it because the JWT's tenant_id didn't match. Now passes tenantId from the server component as a prop. Also adds school-owner-flow E2E test spec. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4edbdd2 commit 1e10366

3 files changed

Lines changed: 173 additions & 0 deletions

File tree

app/[locale]/dashboard/student/courses/[courseId]/exams/[examId]/exam-taker.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface ExamTakerProps {
3737
description: string | null
3838
duration: number | null
3939
questions: Question[]
40+
tenantId: string
4041
}
4142

4243
export function ExamTaker({
@@ -46,6 +47,7 @@ export function ExamTaker({
4647
description,
4748
duration,
4849
questions,
50+
tenantId,
4951
}: ExamTakerProps) {
5052
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
5153
const [answers, setAnswers] = useState<Record<number, string>>({})
@@ -106,6 +108,7 @@ export function ExamTaker({
106108
.insert({
107109
exam_id: examId,
108110
student_id: user.id,
111+
tenant_id: tenantId,
109112
})
110113
.select('submission_id')
111114
.single()

app/[locale]/dashboard/student/courses/[courseId]/exams/[examId]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export default async function TakeExamPage({ params }: PageProps) {
104104
description={exam.description}
105105
duration={exam.duration}
106106
questions={formattedQuestions}
107+
tenantId={tenantId}
107108
/>
108109
)
109110
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { test, expect } from '@playwright/test'
2+
import { loginAsTeacher } from './utils/auth'
3+
import { BASE, LOCALE } from './utils/constants'
4+
5+
/**
6+
* P0 — School Owner Core Flows
7+
*
8+
* Tests the critical path for a school owner (admin):
9+
* 1. Login → dashboard accessible
10+
* 2. Create course → redirected to course detail (not "Course not found")
11+
* 3. Create lesson in that course
12+
* 4. Create exam in that course
13+
* 5. Create exercise in that course
14+
* 6. Course settings accessible
15+
* 7. Courses list shows the new course
16+
*
17+
* Uses owner@e2etest.com (admin role on default tenant).
18+
*/
19+
20+
test.describe('School Owner Core Flows', () => {
21+
let courseId: string
22+
23+
test.beforeEach(async ({ page }) => {
24+
await loginAsTeacher(page)
25+
})
26+
27+
test('1. admin dashboard loads', async ({ page }) => {
28+
await page.goto(`${BASE}/${LOCALE}/dashboard/admin`)
29+
// Should not redirect away or show error
30+
await expect(page).toHaveURL(/\/dashboard\/admin/)
31+
})
32+
33+
test('2. create course → lands on course detail page', async ({ page }) => {
34+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/new`)
35+
await page.getByLabel(/title/i).first().fill('E2E Test Course')
36+
await page.getByLabel(/description/i).first().fill('Created by E2E test')
37+
38+
// Submit
39+
await page.locator('button[type="submit"]').click()
40+
41+
// Should redirect to the course detail page (not "Course not found")
42+
await page.waitForURL(/\/dashboard\/teacher\/courses\/\d+/, { timeout: 15_000 })
43+
courseId = page.url().match(/\/courses\/(\d+)/)?.[1] || ''
44+
expect(courseId).toBeTruthy()
45+
46+
// The course title should be visible
47+
await expect(page.getByRole('heading', { name: 'E2E Test Course' })).toBeVisible({ timeout: 10_000 })
48+
49+
// "Course not found" should NOT be present
50+
await expect(page.getByText('Course not found')).not.toBeVisible()
51+
await expect(page.getByText("doesn't exist")).not.toBeVisible()
52+
})
53+
54+
test('3. create lesson in course', async ({ page }) => {
55+
// First create a course to get an ID
56+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/new`)
57+
await page.getByLabel(/title/i).first().fill('Lesson Test Course')
58+
await page.locator('button[type="submit"]').click()
59+
await page.waitForURL(/\/dashboard\/teacher\/courses\/\d+/, { timeout: 15_000 })
60+
const cId = page.url().match(/\/courses\/(\d+)/)?.[1]
61+
62+
// Navigate to add lesson
63+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/${cId}/lessons/new`)
64+
65+
// Fill lesson form — use placeholder-based selector for title
66+
await page.getByPlaceholder(/introduction to/i).fill('E2E Lesson One')
67+
const summaryField = page.getByPlaceholder(/one-line overview/i)
68+
if (await summaryField.isVisible()) {
69+
await summaryField.fill('A test lesson created by E2E')
70+
}
71+
72+
// Save draft
73+
await page.getByRole('button', { name: /save draft/i }).click()
74+
75+
// Wait for save confirmation (toast or redirect)
76+
await page.waitForTimeout(3000)
77+
78+
// Navigate back to course — lesson should appear
79+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/${cId}`)
80+
await expect(page.getByText('E2E Lesson One')).toBeVisible({ timeout: 10_000 })
81+
})
82+
83+
test('4. create exam in course', async ({ page }) => {
84+
// Create course
85+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/new`)
86+
await page.getByLabel(/title/i).first().fill('Exam Test Course')
87+
await page.locator('button[type="submit"]').click()
88+
await page.waitForURL(/\/dashboard\/teacher\/courses\/\d+/, { timeout: 15_000 })
89+
const cId = page.url().match(/\/courses\/(\d+)/)?.[1]
90+
91+
// Navigate to add exam
92+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/${cId}/exams/new`)
93+
94+
// Fill exam form
95+
await page.getByLabel(/exam title/i).fill('E2E Exam One')
96+
const descField = page.getByLabel(/description/i)
97+
if (await descField.isVisible()) {
98+
await descField.fill('Test exam')
99+
}
100+
101+
// Save
102+
await page.getByRole('button', { name: /save draft/i }).click()
103+
104+
// Should redirect back to course
105+
await page.waitForURL(/\/dashboard\/teacher\/courses\/\d+$/, { timeout: 15_000 })
106+
107+
// Click Exams tab and verify
108+
await page.getByRole('tab', { name: /exams/i }).click()
109+
await expect(page.getByText('E2E Exam One')).toBeVisible({ timeout: 10_000 })
110+
})
111+
112+
test('5. create exercise in course', async ({ page }) => {
113+
// Create course
114+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/new`)
115+
await page.getByLabel(/title/i).first().fill('Exercise Test Course')
116+
await page.locator('button[type="submit"]').click()
117+
await page.waitForURL(/\/dashboard\/teacher\/courses\/\d+/, { timeout: 15_000 })
118+
const cId = page.url().match(/\/courses\/(\d+)/)?.[1]
119+
120+
// Navigate to add exercise
121+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/${cId}/exercises/new`)
122+
123+
// Fill exercise form — title uses placeholder
124+
await page.getByPlaceholder(/build a todo/i).fill('E2E Exercise One')
125+
const descField = page.getByPlaceholder(/what will students/i)
126+
if (await descField.isVisible()) {
127+
await descField.fill('Test exercise')
128+
}
129+
130+
// Save
131+
await page.getByRole('button', { name: /save draft/i }).click()
132+
await page.waitForTimeout(3000)
133+
134+
// Navigate back and verify
135+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/${cId}`)
136+
await page.getByRole('tab', { name: /exercises/i }).click()
137+
await expect(page.getByText('E2E Exercise One')).toBeVisible({ timeout: 10_000 })
138+
})
139+
140+
test('6. course settings page accessible', async ({ page }) => {
141+
// Create course
142+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/new`)
143+
await page.getByLabel(/title/i).first().fill('Settings Test Course')
144+
await page.locator('button[type="submit"]').click()
145+
await page.waitForURL(/\/dashboard\/teacher\/courses\/\d+/, { timeout: 15_000 })
146+
const cId = page.url().match(/\/courses\/(\d+)/)?.[1]
147+
148+
// Navigate to settings
149+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/${cId}/settings`)
150+
151+
// Should load without error
152+
await expect(page.getByText('Course not found')).not.toBeVisible()
153+
await expect(page.getByText("doesn't exist")).not.toBeVisible()
154+
})
155+
156+
test('7. my courses list shows created courses', async ({ page }) => {
157+
// Create a course first
158+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses/new`)
159+
await page.getByLabel(/title/i).first().fill('Listed Course')
160+
await page.locator('button[type="submit"]').click()
161+
await page.waitForURL(/\/dashboard\/teacher\/courses\/\d+/, { timeout: 15_000 })
162+
163+
// Navigate to courses list
164+
await page.goto(`${BASE}/${LOCALE}/dashboard/teacher/courses`)
165+
166+
// Should show the course
167+
await expect(page.getByText('Listed Course')).toBeVisible({ timeout: 10_000 })
168+
})
169+
})

0 commit comments

Comments
 (0)