feat: implement permissions and actions management
- Add permissions logic for status options in corrective actions. - Create tests for permissions functionality. - Implement queries for user authentication and actions management. - Develop Actions page to display and manage corrective actions. - Create authentication pages for login, setup, and password reset. - Add IncidentDetail page to view incident details and associated actions. - Implement Settings page for user management and password changes.
This commit is contained in:
1 parent
708da6b9f7
commit
3f6dff5f24
94 files changed
+7213
-643
No files matched your search
@@ -108,27 +108,62 @@ describe('listing', () => {
|
||||
})
|
||||
|
||||
describe('updating', () => {
|
||||
it('lets supervisors update and close incidents, recording what changed', async () => {
|
||||
it('lets supervisors edit details, recording what changed', async () => {
|
||||
const created = await reporter.post('/api/incidents', report({ title: 'Wet floor' }))
|
||||
const number = created.body.incident.number
|
||||
expect((await reporter.patch(`/api/incidents/${number}`, { status: 'closed' })).status).toBe(403)
|
||||
expect((await reporter.patch(`/api/incidents/${number}`, { severity: 'low' })).status).toBe(403)
|
||||
|
||||
const closed = await supervisor.patch(`/api/incidents/${number}`, { status: 'closed', severity: 'low' })
|
||||
expect(closed.status).toBe(200)
|
||||
expect(closed.body.incident.status).toBe('closed')
|
||||
expect(closed.body.incident.closedAt).not.toBeNull()
|
||||
const edited = await supervisor.patch(`/api/incidents/${number}`, { severity: 'low', title: 'Wet floor by entrance' })
|
||||
expect(edited.status).toBe(200)
|
||||
expect(edited.body.incident).toMatchObject({ severity: 'low', title: 'Wet floor by entrance' })
|
||||
|
||||
const [entry] = await ctx.db.select().from(auditLog).where(eq(auditLog.action, 'update'))
|
||||
expect(entry.changes).toMatchObject({ status: ['reported', 'closed'], severity: ['high', 'low'] })
|
||||
expect(entry.changes).toMatchObject({ severity: ['high', 'low'], title: ['Wet floor', 'Wet floor by entrance'] })
|
||||
})
|
||||
|
||||
const reopened = await supervisor.patch(`/api/incidents/${number}`, { status: 'investigation' })
|
||||
expect(reopened.body.incident.closedAt).toBeNull()
|
||||
it('does not change status through edits', async () => {
|
||||
const created = await reporter.post('/api/incidents', report())
|
||||
const res = await supervisor.patch(`/api/incidents/${created.body.incident.number}`, { status: 'closed' })
|
||||
expect(res.body.incident.status).toBe('reported')
|
||||
})
|
||||
|
||||
it('clears the area when moving an incident to another site', async () => {
|
||||
const created = await reporter.post('/api/incidents', report({ areaId: area.id }))
|
||||
const south = await ctx.createSite('West Campus')
|
||||
const moved = await supervisor.patch(`/api/incidents/${created.body.incident.number}`, { siteId: south.id })
|
||||
expect(moved.body.incident).toMatchObject({ site: { id: south.id }, area: null })
|
||||
const west = await ctx.createSite('West Campus')
|
||||
const moved = await supervisor.patch(`/api/incidents/${created.body.incident.number}`, { siteId: west.id })
|
||||
expect(moved.body.incident).toMatchObject({ site: { id: west.id }, area: null })
|
||||
})
|
||||
})
|
||||
|
||||
describe('notifications', () => {
|
||||
it('alerts safety managers about high-severity reports, not low ones', async () => {
|
||||
const manager = await ctx.createUser('safety_manager', 'alerts@example.com')
|
||||
await reporter.post('/api/incidents', report({ title: 'Arm caught in conveyor', type: 'injury', severity: 'critical' }))
|
||||
await reporter.post('/api/incidents', report({ title: 'Scuffed paint', severity: 'low' }))
|
||||
await ctx.notifier.flush()
|
||||
const mails = ctx.mailer.sent.filter((mail) => mail.to === manager.user.email)
|
||||
expect(mails).toHaveLength(1)
|
||||
expect(mails[0].subject).toMatch(/^\[Critical\] INC-\d{4}: Arm caught in conveyor$/)
|
||||
expect(mails[0].text).toContain('http://safety.test/incidents/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('export', () => {
|
||||
it('exports the filtered list as CSV, neutralising spreadsheet formulas', async () => {
|
||||
await reporter.post('/api/incidents', report({ title: '=HYPERLINK("http://evil")', type: 'environmental', severity: 'low' }))
|
||||
const res = await supervisor.get('/api/incidents/export?type=environmental')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('content-type')).toContain('text/csv')
|
||||
const csv = res.body.toString('utf8')
|
||||
const lines = csv.trimEnd().split('\r\n')
|
||||
expect(lines[0]).toMatch(/^\uFEFFReference,Title,Type/)
|
||||
expect(lines).toHaveLength(2)
|
||||
expect(lines[1]).toContain(`"'=HYPERLINK(""http://evil"")"`)
|
||||
})
|
||||
|
||||
it('only exports what the user can see', async () => {
|
||||
const other = await ctx.createUser('reporter', 'csv-reporter@example.com')
|
||||
const res = await other.get('/api/incidents/export')
|
||||
expect(res.body.toString('utf8').trimEnd().split('\r\n')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user