Skip to content

Commit 276d2fa

Browse files
authored
Automatic thesis anonymization after 5-year retention period (#873)
1 parent cf51135 commit 276d2fa

49 files changed

Lines changed: 2802 additions & 274 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { test, expect } from '@playwright/test'
2+
import { authStatePath, navigateTo, navigateToDetail } from './helpers'
3+
4+
const EXPIRED_THESIS_ID = '00000000-0000-4000-d000-000000000008'
5+
const ALREADY_ANONYMIZED_THESIS_ID = '00000000-0000-4000-d000-000000000009'
6+
const RECENT_FINISHED_THESIS_ID = '00000000-0000-4000-d000-000000000004'
7+
// Thesis 1 — always available, not anonymized, supervisor has SUPERVISOR role on it
8+
const NON_ANONYMIZED_THESIS_ID = '00000000-0000-4000-d000-000000000001'
9+
10+
test.describe('Thesis Anonymization - Admin Batch Operations', () => {
11+
test.use({ storageState: authStatePath('admin') })
12+
13+
test.describe.configure({ mode: 'serial' })
14+
15+
test('admin can trigger thesis anonymization from admin page', async ({ page }) => {
16+
await navigateTo(page, '/admin')
17+
18+
await expect(page.getByRole('heading', { name: 'Administration' })).toBeVisible({
19+
timeout: 30_000,
20+
})
21+
22+
// Verify the Thesis Anonymization section is present with description
23+
await expect(page.getByRole('heading', { name: 'Thesis Anonymization' })).toBeVisible()
24+
await expect(page.getByText(/Anonymize theses that have exceeded/i)).toBeVisible()
25+
await expect(page.getByText(/5-year legal retention period/i)).toBeVisible()
26+
27+
const anonymizeButton = page.getByRole('button', { name: 'Run Anonymization' })
28+
await expect(anonymizeButton).toBeVisible()
29+
await anonymizeButton.click()
30+
31+
// Thesis 8 (expired retention) should be anonymized
32+
await expect(page.getByText(/Anonymized \d+ expired thes(is|es)/)).toBeVisible({
33+
timeout: 15_000,
34+
})
35+
})
36+
37+
test('second anonymization run finds no new theses (idempotent)', async ({ page }) => {
38+
await navigateTo(page, '/admin')
39+
40+
await expect(page.getByRole('heading', { name: 'Administration' })).toBeVisible({
41+
timeout: 30_000,
42+
})
43+
44+
const anonymizeButton = page.getByRole('button', { name: 'Run Anonymization' })
45+
await anonymizeButton.click()
46+
47+
// After the first run already anonymized everything, second run should find nothing
48+
await expect(page.getByText('No expired theses found')).toBeVisible({ timeout: 15_000 })
49+
})
50+
51+
test('batch-anonymized thesis shows banner on detail page', async ({ page }) => {
52+
// Thesis 8 was just anonymized by the batch run above
53+
await navigateTo(page, `/theses/${EXPIRED_THESIS_ID}`)
54+
55+
const banner = page.getByText(/This thesis was anonymized on/i)
56+
await expect(banner).toBeVisible({ timeout: 30_000 })
57+
await expect(page.getByText(/per data retention policy/i)).toBeVisible()
58+
await expect(page.getByText(/permanently removed/i)).toBeVisible()
59+
})
60+
61+
test('batch-anonymized thesis preserves structural data', async ({ page }) => {
62+
// Thesis 8 was batch-anonymized — structural fields should remain
63+
const heading = page.getByRole('heading', { name: /Legacy Data Processing Pipeline/i })
64+
const loaded = await navigateToDetail(page, `/theses/${EXPIRED_THESIS_ID}`, heading, 30_000)
65+
expect(loaded).toBeTruthy()
66+
67+
// Title should still be visible
68+
await expect(heading).toBeVisible()
69+
70+
// Anonymization banner should be present (confirming it was anonymized)
71+
await expect(page.getByText(/This thesis was anonymized on/i)).toBeVisible()
72+
})
73+
})
74+
75+
test.describe('Thesis Anonymization - Pre-Anonymized Thesis Banner', () => {
76+
test.use({ storageState: authStatePath('admin') })
77+
78+
test('pre-anonymized thesis shows banner with correct content', async ({ page }) => {
79+
await navigateTo(page, `/theses/${ALREADY_ANONYMIZED_THESIS_ID}`)
80+
81+
// Verify the anonymization banner is displayed with correct content
82+
const banner = page.getByText(/This thesis was anonymized on/i)
83+
await expect(banner).toBeVisible({ timeout: 30_000 })
84+
await expect(page.getByText(/per data retention policy/i)).toBeVisible()
85+
await expect(page.getByText(/permanently removed/i)).toBeVisible()
86+
})
87+
88+
test('anonymize button is hidden on already-anonymized thesis', async ({ page }) => {
89+
const heading = page.getByRole('heading', { name: /Archived Research on Software/i })
90+
const loaded = await navigateToDetail(
91+
page,
92+
`/theses/${ALREADY_ANONYMIZED_THESIS_ID}`,
93+
heading,
94+
30_000,
95+
)
96+
expect(loaded).toBeTruthy()
97+
98+
// Open Configuration accordion
99+
await page.getByText('Configuration').click()
100+
101+
// Admin should see Update button but NOT Anonymize Thesis (already anonymized)
102+
await expect(page.getByRole('button', { name: 'Update' })).toBeVisible({ timeout: 5_000 })
103+
await expect(page.getByRole('button', { name: 'Anonymize Thesis' })).not.toBeVisible({
104+
timeout: 3_000,
105+
})
106+
})
107+
})
108+
109+
test.describe('Thesis Anonymization - Non-Anonymized Thesis', () => {
110+
test.use({ storageState: authStatePath('admin') })
111+
112+
test('recently finished thesis is not affected by anonymization', async ({ page }) => {
113+
// Thesis 4 finished 60 days ago — well within the 5-year retention period
114+
const heading = page.getByRole('heading', { name: /Systematic Monolith/i })
115+
const loaded = await navigateToDetail(
116+
page,
117+
`/theses/${RECENT_FINISHED_THESIS_ID}`,
118+
heading,
119+
30_000,
120+
)
121+
expect(loaded).toBeTruthy()
122+
123+
// Should NOT show anonymization banner
124+
await expect(page.getByText(/This thesis was anonymized on/i)).not.toBeVisible({
125+
timeout: 3_000,
126+
})
127+
128+
// Should still have its data intact (thesis title is visible)
129+
await expect(heading).toBeVisible()
130+
})
131+
132+
test('non-anonymized thesis shows anonymize button for admin', async ({ page }) => {
133+
const heading = page.getByRole('heading', { name: /Automated Code Review/i })
134+
const loaded = await navigateToDetail(
135+
page,
136+
`/theses/${NON_ANONYMIZED_THESIS_ID}`,
137+
heading,
138+
30_000,
139+
)
140+
expect(loaded).toBeTruthy()
141+
142+
// Open Configuration accordion
143+
await page.getByText('Configuration').click()
144+
145+
// Admin should see the Anonymize Thesis button on non-anonymized thesis
146+
await expect(page.getByRole('button', { name: 'Anonymize Thesis' })).toBeVisible({
147+
timeout: 5_000,
148+
})
149+
})
150+
})
151+
152+
test.describe('Thesis Anonymization - Non-Admin Restrictions', () => {
153+
test.use({ storageState: authStatePath('student') })
154+
155+
test('student cannot access admin page for batch anonymization', async ({ page }) => {
156+
await navigateTo(page, '/admin')
157+
158+
await expect(page.getByRole('heading', { name: 'Administration' })).not.toBeVisible({
159+
timeout: 10_000,
160+
})
161+
})
162+
})
163+
164+
test.describe('Thesis Anonymization - Advisor Restrictions', () => {
165+
test.use({ storageState: authStatePath('advisor') })
166+
167+
test('advisor cannot access admin page for batch anonymization', async ({ page }) => {
168+
await navigateTo(page, '/admin')
169+
170+
await expect(page.getByRole('heading', { name: 'Administration' })).not.toBeVisible({
171+
timeout: 10_000,
172+
})
173+
})
174+
175+
test('advisor does not see anonymize button on thesis detail', async ({ page }) => {
176+
const heading = page.getByRole('heading', { name: /Automated Code Review/i })
177+
const loaded = await navigateToDetail(
178+
page,
179+
`/theses/${NON_ANONYMIZED_THESIS_ID}`,
180+
heading,
181+
30_000,
182+
)
183+
expect(loaded).toBeTruthy()
184+
185+
// Open Configuration accordion
186+
await page.getByText('Configuration').click()
187+
188+
// Advisor should see Update button but NOT Anonymize Thesis
189+
await expect(page.getByRole('button', { name: 'Update' })).toBeVisible({ timeout: 5_000 })
190+
await expect(page.getByRole('button', { name: 'Anonymize Thesis' })).not.toBeVisible({
191+
timeout: 3_000,
192+
})
193+
})
194+
})

0 commit comments

Comments
 (0)