From fa6dc11fee47bb1f82bbb1c382ed8d02ca5e0e85 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 20 Jul 2026 15:01:14 +0300 Subject: [PATCH] Update backend services, user right tests and user right documentation (#1217) * update some backend tests slightly, and update user right docs * improve locality updating tests * refactor species merge tests, and add more user rights tests to merging * fix locality user right issue with users being able to bypass project membership requirements by adding new projects when updating a project, and add tests * fix small, copilot suggested issues * change bigint value back to number, and fix hanging test * add error content checks and get request test for regions * add error content checks and reorder tests in time bound/time unit * add person get tests, update delete tests, and update user right docs * update museum tests * update testing docs * add tests for all user groups in project delete tests * add more user right tests to project/ and projects/ * Fix pid test Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Rename project get test describe block Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../collectingMethodValues/getAll.test.ts | 20 +- backend/src/api-tests/crossSearch/get.test.ts | 2 +- backend/src/api-tests/locality/data.ts | 16 -- backend/src/api-tests/locality/update.test.ts | 182 ++++++++++++------ backend/src/api-tests/museum/create.test.ts | 22 ++- backend/src/api-tests/museum/update.test.ts | 20 +- backend/src/api-tests/person/delete.test.ts | 5 + backend/src/api-tests/person/get.test.ts | 69 +++++++ backend/src/api-tests/project/delete.test.ts | 26 ++- backend/src/api-tests/projects/create.test.ts | 28 ++- backend/src/api-tests/projects/get.test.ts | 39 ++++ backend/src/api-tests/projects/update.test.ts | 31 +-- backend/src/api-tests/region/delete.test.ts | 5 +- backend/src/api-tests/region/get.test.ts | 22 ++- backend/src/api-tests/sequence.test.ts | 7 +- backend/src/api-tests/species/merge.test.ts | 162 +++++++--------- .../src/api-tests/timeBound/create.test.ts | 32 +-- .../src/api-tests/timeBound/delete.test.ts | 4 +- backend/src/api-tests/timeUnit/create.test.ts | 34 ++-- backend/src/api-tests/timeUnit/delete.test.ts | 4 +- backend/src/services/locality.ts | 12 +- documentation/testing.md | 5 + documentation/user_rights.md | 16 +- 23 files changed, 509 insertions(+), 254 deletions(-) create mode 100644 backend/src/api-tests/person/get.test.ts create mode 100644 backend/src/api-tests/projects/get.test.ts diff --git a/backend/src/api-tests/collectingMethodValues/getAll.test.ts b/backend/src/api-tests/collectingMethodValues/getAll.test.ts index be674f45..07453455 100644 --- a/backend/src/api-tests/collectingMethodValues/getAll.test.ts +++ b/backend/src/api-tests/collectingMethodValues/getAll.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals' -import { login, noPermError, resetDatabase, resetDatabaseTimeout, send } from '../utils' +import { login, logout, noPermError, resetDatabase, resetDatabaseTimeout, send } from '../utils' import { pool } from '../../utils/db' type CollectingMethodValue = { coll_meth_value: string } @@ -27,12 +27,20 @@ describe('GET /collecting-method-values/all', () => { expect(values).toEqual(sorted) }) - it('rejects users without locality edit access', async () => { - await login('testPl', 'test') + it('rejects anonymous users', async () => { + logout() + const { body: noUserBody, status: noUserStatus } = await send('collecting-method-values/all', 'GET') + expect(noUserStatus).toBe(403) + expect(noUserBody).toEqual(noPermError) + }) - const { body, status } = await send('collecting-method-values/all', 'GET') + it('accepts users with locality edit access', async () => { + await login('testEr', 'test') + const { status: erStatus } = await send('collecting-method-values/all', 'GET') + expect(erStatus).toBe(200) - expect(status).toBe(403) - expect(body).toEqual(noPermError) + await login('testEu', 'test') + const { status: euStatus } = await send('collecting-method-values/all', 'GET') + expect(euStatus).toBe(200) }) }) diff --git a/backend/src/api-tests/crossSearch/get.test.ts b/backend/src/api-tests/crossSearch/get.test.ts index 0003438c..5d1eedde 100644 --- a/backend/src/api-tests/crossSearch/get.test.ts +++ b/backend/src/api-tests/crossSearch/get.test.ts @@ -240,7 +240,7 @@ describe('Getting cross-search data', () => { }) describe('does not show drafts without proper user rights', () => { - it('Get all with admin has correct amount of data', async () => { + it('Get all with admin has correct data', async () => { await login() const response = await send(`crosssearch/all/999/0/[]/[]`, 'GET') expect(response.status).toEqual(200) diff --git a/backend/src/api-tests/locality/data.ts b/backend/src/api-tests/locality/data.ts index a9049885..fc509a9b 100644 --- a/backend/src/api-tests/locality/data.ts +++ b/backend/src/api-tests/locality/data.ts @@ -218,22 +218,6 @@ export const editedLocality = { lid: 21050, loc_name: 'New test name', now_mus: [], - now_ls: [ - { - rowState: 'new', - species_id: 21052, - lid: 21050, - com_species: { species_id: 21052 } as SpeciesDetailsType, - }, - { - rowState: 'removed', - species_id: 85730, - lid: 21050, - com_species: { species_id: 85730 } as SpeciesDetailsType, - }, - ], - projects: [], - now_plr: [], now_ss: [], now_coll_meth: [], now_lau: [], diff --git a/backend/src/api-tests/locality/update.test.ts b/backend/src/api-tests/locality/update.test.ts index 23b99b5b..802b5b37 100644 --- a/backend/src/api-tests/locality/update.test.ts +++ b/backend/src/api-tests/locality/update.test.ts @@ -9,7 +9,7 @@ import { invalidPollenUpdateLocality, newLocalityBasis, } from './data' -import { login, resetDatabase, send, resetDatabaseTimeout } from '../utils' +import { login, logout, resetDatabase, send, resetDatabaseTimeout, noPermError } from '../utils' import { pool } from '../../utils/db' let resultLocality: LocalityDetailsType | null = null @@ -21,22 +21,38 @@ const buildUpdatePayload = (overrides: Record = {}) => ({ ...overrides, }) -describe('Locality update works', () => { +describe('Locality update', () => { beforeAll(async () => { await resetDatabase() }, resetDatabaseTimeout) beforeEach(async () => { - await resetDatabase() await login() - resultLocality = null }) afterAll(async () => { await pool.end() }) - describe('with a successful locality update applied', () => { - beforeEach(async () => { - const writeResult = await send<{ id: number }>('locality', 'PUT', { locality: buildUpdatePayload() }) + describe('works and updates data correctly', () => { + beforeAll(async () => { + await login() + const writeResult = await send<{ id: number }>('locality', 'PUT', { + locality: buildUpdatePayload({ + now_ls: [ + { + rowState: 'new', + species_id: 21052, + lid: 21050, + com_species: { species_id: 21052 }, + }, + { + rowState: 'removed', + species_id: 85730, + lid: 21050, + com_species: { species_id: 85730 }, + }, + ], + }), + }) expect(writeResult.status).toEqual(200) expect(writeResult.body.id).toEqual(editedLocality.lid) // `Invalid result returned on write: ${writeResult.body.id}` @@ -117,70 +133,118 @@ describe('Locality update works', () => { }) }) - it('Clearing an optional text field persists null', async () => { - const writeResult = await send<{ id: number }>('locality', 'PUT', { - locality: buildUpdatePayload({ rock_type: '' }), + describe('works with edge cases, and fails with invalid values or permissions', () => { + beforeAll(async () => { + await resetDatabase() + await login() + const writeResult = await send<{ id: number }>('locality', 'PUT', { + locality: buildUpdatePayload(), + }) + + expect(writeResult.status).toEqual(200) + expect(writeResult.body.id).toEqual(editedLocality.lid) // `Invalid result returned on write: ${writeResult.body.id}` }) - expect(writeResult.status).toEqual(200) - expect(writeResult.body.id).toEqual(editedLocality.lid) + it('Clearing an optional text field persists null', async () => { + const writeResult = await send<{ id: number }>('locality', 'PUT', { + locality: buildUpdatePayload({ rock_type: '' }), + }) - const { body } = await send(`locality/${editedLocality.lid}`, 'GET') - expect(body.rock_type).toBeNull() - }) + expect(writeResult.status).toEqual(200) + expect(writeResult.body.id).toEqual(editedLocality.lid) - it('Update fails when pollen values are out of range', async () => { - const { body, status } = await send('locality', 'PUT', { - locality: buildUpdatePayload(invalidPollenUpdateLocality), + const { body } = await send(`locality/${editedLocality.lid}`, 'GET') + expect(body.rock_type).toBeNull() }) - expect(status).toEqual(403) - expect(body).toEqual( - expect.arrayContaining([ - { - name: 'Arboreal pollen (AP%)', - error: 'Arboreal pollen (AP%) must be between 0 and 100', - }, - ]) - ) - }) + it('Update fails when pollen values are out of range', async () => { + const { body, status } = await send('locality', 'PUT', { + locality: buildUpdatePayload(invalidPollenUpdateLocality), + }) - it('Update fails when pollen total exceeds 100', async () => { - const { body, status } = await send('locality', 'PUT', { - locality: buildUpdatePayload(invalidPollenTotalUpdateLocality), + expect(status).toEqual(403) + expect(body).toEqual( + expect.arrayContaining([ + { + name: 'Arboreal pollen (AP%)', + error: 'Arboreal pollen (AP%) must be between 0 and 100', + }, + ]) + ) }) - expect(status).toEqual(403) - expect(body).toEqual( - expect.arrayContaining([ - { - name: 'Arboreal pollen (AP%)', - error: - 'Combined Arboreal (AP%), Non-arboreal (NAP%), and Other pollen (OP%) must be less than or equal to 100', - }, - ]) - ) - }) + it('Update fails when pollen total exceeds 100', async () => { + const { body, status } = await send('locality', 'PUT', { + locality: buildUpdatePayload(invalidPollenTotalUpdateLocality), + }) - it('Update fails when estimate temperature is out of range', async () => { - const { body, status } = await send('locality', 'PUT', { - locality: buildUpdatePayload(invalidEstimateTempUpdateLocality), + expect(status).toEqual(403) + expect(body).toEqual( + expect.arrayContaining([ + { + name: 'Arboreal pollen (AP%)', + error: + 'Combined Arboreal (AP%), Non-arboreal (NAP%), and Other pollen (OP%) must be less than or equal to 100', + }, + ]) + ) }) - expect(status).toEqual(403) - expect(body).toEqual( - expect.arrayContaining([ - { - name: 'Estimated temperature', - error: 'Estimated temperature must be between -999.9 and 999.9', - }, - ]) - ) - }) + it('Update fails when estimate temperature is out of range', async () => { + const { body, status } = await send('locality', 'PUT', { + locality: buildUpdatePayload(invalidEstimateTempUpdateLocality), + }) - it('Editing locality without changing anything should succeed', async () => { - const writeResult = await send<{ id: number }>('locality', 'PUT', { locality: buildUpdatePayload() }) - expect(writeResult.status).toEqual(200) - expect(writeResult.body.id).toEqual(editedLocality.lid) // `Invalid result returned on write: ${writeResult.body.id} + expect(status).toEqual(403) + expect(body).toEqual( + expect.arrayContaining([ + { + name: 'Estimated temperature', + error: 'Estimated temperature must be between -999.9 and 999.9', + }, + ]) + ) + }) + + it('Editing locality without changing anything should succeed', async () => { + const writeResult = await send<{ id: number }>('locality', 'PUT', { locality: buildUpdatePayload() }) + expect(writeResult.status).toEqual(200) + expect(writeResult.body.id).toEqual(editedLocality.lid) // `Invalid result returned on write: ${writeResult.body.id} + }) + + it('Editing locality as an editrestricted user that belongs in the same project as the locality succeeds', async () => { + logout() + await login('testEr', 'test') + const erWriteResult = await send<{ id: number }>('locality', 'PUT', { locality: buildUpdatePayload() }) + // testEr belongs to the NOW Database project, which is already linked to the locality + expect(erWriteResult.status).toEqual(200) + }) + + it('Editing locality fails as an anonymous user', async () => { + logout() + const writeResult = await send<{ id: number }>('locality', 'PUT', { locality: buildUpdatePayload() }) + expect(writeResult.status).toEqual(403) + }) + + it('Editing locality fails as an EditRestricted user, when the locality is not in the same project as the user', async () => { + const changeProjectResult = await send<{ id: number }>('locality', 'PUT', { + locality: buildUpdatePayload({ now_plr: [{ pid: 35, rowState: 'new' }] }), + }) + expect(changeProjectResult.status).toBe(200) + + logout() + await login('testEr', 'test') + const resultEr = await send('locality', 'PUT', { + locality: buildUpdatePayload(), + }) + expect(resultEr.body).toEqual(noPermError) + expect(resultEr.status).toEqual(403) + + const resultErWithAddedPlr = await send('locality', 'PUT', { + locality: buildUpdatePayload({ now_plr: [{ pid: 3, rowState: 'new' }] }), + }) + expect(resultErWithAddedPlr.body).toEqual(noPermError) + expect(resultErWithAddedPlr.status).toEqual(403) + }) }) }) diff --git a/backend/src/api-tests/museum/create.test.ts b/backend/src/api-tests/museum/create.test.ts index 32cf3dfa..56c1467a 100644 --- a/backend/src/api-tests/museum/create.test.ts +++ b/backend/src/api-tests/museum/create.test.ts @@ -54,6 +54,7 @@ describe('Creating new museum works', () => { expect(getReqStatus).toEqual(409) expect(resultBody.code).toEqual('duplicate_museum_code') }) + it('Creation fails with empty museum code', async () => { const { body: resultBody, status: getReqStatus } = await send('museum/', 'PUT', { museum: { ...newMuseumBasis, museum: null }, @@ -62,26 +63,29 @@ describe('Creating new museum works', () => { expect(resultBody.length).toEqual(1) //There should be 1 validation error }) - it('Creation fails without permissions', async () => { + it('Creation succeeds for all logged in users', async () => { logout() - const { body: resultBodyNoPerm, status: resultStatusNoPerm } = await send('museum/', 'PUT', { - museum: { ...newMuseumBasis, museum: 'New Museum 2' }, - }) - expect(resultBodyNoPerm).toEqual(noPermError) - expect(resultStatusNoPerm).toEqual(403) - await login('testEr') const { body: resultBodyEr, status: resultStatusEr } = await send<{ museum: string }>('museum/', 'PUT', { - museum: { ...newMuseumBasis, museum: 'NM3' }, + museum: { ...newMuseumBasis, museum: 'NewMuseum2' }, }) expect(resultStatusEr).toEqual(200) expect(typeof resultBodyEr.museum).toEqual('string') await login('testEu') const { body: resultBodyEu, status: resultStatusEu } = await send<{ museum: string }>('museum/', 'PUT', { - museum: { ...newMuseumBasis, museum: 'NM4' }, + museum: { ...newMuseumBasis, museum: 'NM3' }, }) expect(resultStatusEu).toEqual(200) expect(typeof resultBodyEu.museum).toEqual('string') }) + + it('Creation fails for anonymous users', async () => { + logout() + const { body: resultBodyNoPerm, status: resultStatusNoPerm } = await send('museum/', 'PUT', { + museum: { ...newMuseumBasis, museum: 'NM4' }, + }) + expect(resultBodyNoPerm).toEqual(noPermError) + expect(resultStatusNoPerm).toEqual(403) + }) }) diff --git a/backend/src/api-tests/museum/update.test.ts b/backend/src/api-tests/museum/update.test.ts index a0ebd0fc..e4ea2746 100644 --- a/backend/src/api-tests/museum/update.test.ts +++ b/backend/src/api-tests/museum/update.test.ts @@ -47,14 +47,7 @@ describe('Updating museum works', () => { expect(resultBody.length).toEqual(1) //There should be 1 validation error }) - it('Updating fails without permissions', async () => { - logout() - const { body: resultBodyNoPerm, status: resultStatusNoPerm } = await send('museum/', 'PUT', { - museum: { ...editedMuseum, institution: 'New Museum 2' }, - }) - expect(resultBodyNoPerm).toEqual(noPermError) - expect(resultStatusNoPerm).toEqual(403) - + it('Updating succeeds for all logged in users', async () => { await login('testEr') const { body: resultBodyEr, status: resultStatusEr } = await send<{ museum: string }>('museum/', 'PUT', { museum: { ...editedMuseum, institution: 'New Museum 2' }, @@ -64,9 +57,18 @@ describe('Updating museum works', () => { await login('testEu') const { body: resultBodyEu, status: resultStatusEu } = await send<{ museum: string }>('museum/', 'PUT', { - museum: { ...editedMuseum, institution: 'New Museum 2' }, + museum: { ...editedMuseum, institution: 'NM3' }, }) expect(resultStatusEu).toEqual(200) expect(typeof resultBodyEu.museum).toEqual('string') }) + + it('Updating fails for anonymous users', async () => { + logout() + const { body: resultBodyNoPerm, status: resultStatusNoPerm } = await send('museum/', 'PUT', { + museum: { ...editedMuseum, institution: 'NM4' }, + }) + expect(resultBodyNoPerm).toEqual(noPermError) + expect(resultStatusNoPerm).toEqual(403) + }) }) diff --git a/backend/src/api-tests/person/delete.test.ts b/backend/src/api-tests/person/delete.test.ts index 57515316..41448fb1 100644 --- a/backend/src/api-tests/person/delete.test.ts +++ b/backend/src/api-tests/person/delete.test.ts @@ -94,6 +94,11 @@ describe('Deleting person works', () => { expect(deleteResultEr.status).toEqual(403) expect(deleteResultEr.body).toEqual(noPermError) + await login('testEu') + const deleteResultEu = await send('person/DEL-PERM', 'DELETE') + expect(deleteResultEu.status).toEqual(403) + expect(deleteResultEu.body).toEqual(noPermError) + const person = await nowDb.com_people.findUnique({ where: { initials: 'DEL-PERM' } }) expect(person).not.toBeNull() }) diff --git a/backend/src/api-tests/person/get.test.ts b/backend/src/api-tests/person/get.test.ts new file mode 100644 index 00000000..a50df6d0 --- /dev/null +++ b/backend/src/api-tests/person/get.test.ts @@ -0,0 +1,69 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals' +import { pool, nowDb } from '../../utils/db' +import { login, logout, noPermError, resetDatabase, resetDatabaseTimeout, send, unauthenticatedError } from '../utils' +import { existingPerson } from './data' +import { PersonDetails } from '../../../../frontend/src/shared/types' + +describe('Getting a persons information works', () => { + beforeAll(async () => { + await resetDatabase() + const existing = await nowDb.com_people.findUnique({ where: { initials: existingPerson.initials } }) + expect(existing).not.toBeNull() + }, resetDatabaseTimeout) + + beforeEach(async () => { + await login() + }) + + afterAll(async () => { + await pool.end() + }) + + it('getting a list of all persons succeeds with admin permissions', async () => { + const getResultAdmin = await send(`person/all`, 'GET') + expect(getResultAdmin.status).toEqual(200) + expect(getResultAdmin.body.length).toEqual(13) + expect((getResultAdmin.body as PersonDetails[])[0].first_name).toEqual(existingPerson.first_name) + }) + + it('getting person details succeeds with admin permissions', async () => { + const getResultAdmin = await send(`person/${existingPerson.initials}`, 'GET') + expect(getResultAdmin.status).toEqual(200) + expect((getResultAdmin.body as PersonDetails).first_name).toEqual(existingPerson.first_name) + }) + + it('getting a list of all persons fails without admin permissions', async () => { + logout() + const getResultNoPerm = await send(`person/all`, 'GET') + expect(getResultNoPerm.body).toEqual(noPermError) + expect(getResultNoPerm.status).toEqual(403) + + await login('testEr') + const getResultEr = await send(`person/all`, 'GET') + expect(getResultEr.body).toEqual(noPermError) + expect(getResultEr.status).toEqual(403) + + await login('testEu') + const getResultEu = await send(`person/all`, 'GET') + expect(getResultEu.body).toEqual(noPermError) + expect(getResultEu.status).toEqual(403) + }) + + it('getting person details fails without admin permissions', async () => { + logout() + const getResultNoPerm = await send(`person/${existingPerson.initials}`, 'GET') + // this throws a different error since access checking happens differently in person router + expect(getResultNoPerm.body).toEqual(unauthenticatedError) + expect(getResultNoPerm.status).toEqual(401) + + await login('testEr') + const getResultEr = await send(`person/${existingPerson.initials}`, 'GET') + expect(getResultEr.body).toEqual(noPermError) + expect(getResultEr.status).toEqual(403) + + await login('testEu') + const getResultEu = await send(`person/${existingPerson.initials}`, 'GET') + expect(getResultEu.body).toEqual(noPermError) + expect(getResultEu.status).toEqual(403) + }) +}) diff --git a/backend/src/api-tests/project/delete.test.ts b/backend/src/api-tests/project/delete.test.ts index c1d19817..edaae868 100644 --- a/backend/src/api-tests/project/delete.test.ts +++ b/backend/src/api-tests/project/delete.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals' -import { login, resetDatabase, resetDatabaseTimeout, send } from '../utils' +import { login, logout, noPermError, resetDatabase, resetDatabaseTimeout, send } from '../utils' import { pool } from '../../utils/db' describe('DELETE /project/:id', () => { @@ -38,8 +38,28 @@ describe('DELETE /project/:id', () => { }) await login('testPl', 'test') - const deleteAttempt = await send(`project/${createdProject.body.pid}`, 'DELETE') + const plDeleteAttempt = await send(`project/${createdProject.body.pid}`, 'DELETE') + expect(plDeleteAttempt.status).toEqual(403) + expect(plDeleteAttempt.body).toEqual(noPermError) - expect(deleteAttempt.status).toEqual(403) + await login('testEu', 'test') + const euDeleteAttempt = await send(`project/${createdProject.body.pid}`, 'DELETE') + expect(euDeleteAttempt.status).toEqual(403) + expect(euDeleteAttempt.body).toEqual(noPermError) + + await login('testEr', 'test') + const erDeleteAttempt = await send(`project/${createdProject.body.pid}`, 'DELETE') + expect(erDeleteAttempt.status).toEqual(403) + expect(erDeleteAttempt.body).toEqual(noPermError) + + logout() + const anonDeleteAttempt = await send(`project/${createdProject.body.pid}`, 'DELETE') + expect(anonDeleteAttempt.status).toEqual(403) + expect(anonDeleteAttempt.body).toEqual(noPermError) + + await login('testSu', 'test') + const fetchAfterAttempts = await send(`project/${createdProject.body.pid}`, 'GET') + expect(fetchAfterAttempts.status).toEqual(200) + expect(fetchAfterAttempts.body).toHaveProperty('pid', createdProject.body.pid) }) }) diff --git a/backend/src/api-tests/projects/create.test.ts b/backend/src/api-tests/projects/create.test.ts index 1384bca1..83898c6d 100644 --- a/backend/src/api-tests/projects/create.test.ts +++ b/backend/src/api-tests/projects/create.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals' -import { login, resetDatabase, resetDatabaseTimeout, send } from '../utils' +import { login, logout, noPermError, resetDatabase, resetDatabaseTimeout, send } from '../utils' import { pool } from '../../utils/db' describe('POST /projects', () => { @@ -59,14 +59,30 @@ describe('POST /projects', () => { }) it('denies non-admin users', async () => { - await login('testPl', 'test') - - const result = await send('projects', 'POST', { + const unauthorizedProject = { projectCode: 'PRJ-004', projectName: 'Unauthorized Create', coordinatorUserId: 163, - }) + } - expect(result.status).toEqual(403) + await login('testPl', 'test') + const plResult = await send('projects', 'POST', unauthorizedProject) + expect(plResult.status).toEqual(403) + expect(plResult.body).toEqual(noPermError) + + await login('testEu', 'test') + const euResult = await send('projects', 'POST', unauthorizedProject) + expect(euResult.status).toEqual(403) + expect(euResult.body).toEqual(noPermError) + + await login('testEr', 'test') + const erResult = await send('projects', 'POST', unauthorizedProject) + expect(erResult.status).toEqual(403) + expect(erResult.body).toEqual(noPermError) + + logout() + const anonResult = await send('projects', 'POST', unauthorizedProject) + expect(anonResult.status).toEqual(403) + expect(anonResult.body).toEqual(noPermError) }) }) diff --git a/backend/src/api-tests/projects/get.test.ts b/backend/src/api-tests/projects/get.test.ts new file mode 100644 index 00000000..e0489a74 --- /dev/null +++ b/backend/src/api-tests/projects/get.test.ts @@ -0,0 +1,39 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals' +import { login, logout, noPermError, resetDatabase, resetDatabaseTimeout, send } from '../utils' +import { pool } from '../../utils/db' + +describe('GET /project/:id', () => { + beforeAll(async () => { + await resetDatabase() + }, resetDatabaseTimeout) + + afterAll(async () => { + await pool.end() + }) + + it('viewing project details fails for non-admin users', async () => { + await login('testSu', 'test') + const suResult = await send('project/3', 'GET') + expect(suResult.status).toEqual(200) + + await login('testPl', 'test') + const plResult = await send('project/3', 'GET') + expect(plResult.status).toEqual(403) + expect(plResult.body).toEqual(noPermError) + + await login('testEu', 'test') + const euResult = await send('project/3', 'GET') + expect(euResult.status).toEqual(403) + expect(euResult.body).toEqual(noPermError) + + await login('testEr', 'test') + const erResult = await send('project/3', 'GET') + expect(erResult.status).toEqual(403) + expect(erResult.body).toEqual(noPermError) + + logout() + const anonResult = await send('project/3', 'GET') + expect(anonResult.status).toEqual(403) + expect(anonResult.body).toEqual(noPermError) + }) +}) diff --git a/backend/src/api-tests/projects/update.test.ts b/backend/src/api-tests/projects/update.test.ts index 994e1542..54d2d8b9 100644 --- a/backend/src/api-tests/projects/update.test.ts +++ b/backend/src/api-tests/projects/update.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals' -import { login, resetDatabase, resetDatabaseTimeout, send } from '../utils' +import { login, logout, noPermError, resetDatabase, resetDatabaseTimeout, send } from '../utils' import { pool } from '../../utils/db' describe('PUT /projects/:id', () => { @@ -9,6 +9,7 @@ describe('PUT /projects/:id', () => { beforeEach(async () => { await resetDatabase() + await login('testSu', 'test') }) afterAll(async () => { @@ -16,8 +17,6 @@ describe('PUT /projects/:id', () => { }) it('updates coordinator and members', async () => { - await login('testSu', 'test') - const createResult = await send<{ pid: number }>('projects', 'POST', { projectCode: 'PRJ-UPDATE', projectName: 'Project To Update', @@ -47,8 +46,6 @@ describe('PUT /projects/:id', () => { }) it('returns validation error for invalid members', async () => { - await login('testSu', 'test') - const createResult = await send<{ pid: number }>('projects', 'POST', { projectCode: 'PRJ-INV1', projectName: 'Project Invalid Members', @@ -64,8 +61,6 @@ describe('PUT /projects/:id', () => { }) it('returns validation error for too-long project code', async () => { - await login('testSu', 'test') - const createResult = await send<{ pid: number }>('projects', 'POST', { projectCode: 'PRJ-LONG1', projectName: 'Project Long Code', @@ -82,12 +77,26 @@ describe('PUT /projects/:id', () => { }) it('denies non-admin users', async () => { + const unauthorizedUpdate = { projectName: 'Unauthorized Update' } + await login('testPl', 'test') + const plResult = await send('projects/1', 'PUT', unauthorizedUpdate) + expect(plResult.status).toEqual(403) + expect(plResult.body).toEqual(noPermError) - const result = await send('projects/1', 'PUT', { - projectName: 'Unauthorized Update', - }) + await login('testEu', 'test') + const euResult = await send('projects/1', 'PUT', unauthorizedUpdate) + expect(euResult.status).toEqual(403) + expect(euResult.body).toEqual(noPermError) - expect(result.status).toEqual(403) + await login('testEr', 'test') + const erResult = await send('projects/1', 'PUT', unauthorizedUpdate) + expect(erResult.status).toEqual(403) + expect(erResult.body).toEqual(noPermError) + + logout() + const anonResult = await send('projects/1', 'PUT', unauthorizedUpdate) + expect(anonResult.status).toEqual(403) + expect(anonResult.body).toEqual(noPermError) }) }) diff --git a/backend/src/api-tests/region/delete.test.ts b/backend/src/api-tests/region/delete.test.ts index 518339d4..a93e2293 100644 --- a/backend/src/api-tests/region/delete.test.ts +++ b/backend/src/api-tests/region/delete.test.ts @@ -1,5 +1,5 @@ import { beforeEach, beforeAll, afterAll, describe, it, expect } from '@jest/globals' -import { login, logout, resetDatabase, send, resetDatabaseTimeout } from '../utils' +import { login, logout, resetDatabase, send, resetDatabaseTimeout, noPermError } from '../utils' import { pool } from '../../utils/db' describe('Deleting a region works', () => { @@ -23,14 +23,17 @@ describe('Deleting a region works', () => { it('Deleting fails without permissions', async () => { logout() const deleteResultNoPerm = await send<{ id: number }>('region/2', 'DELETE') + expect(deleteResultNoPerm.body).toEqual(noPermError) expect(deleteResultNoPerm.status).toEqual(403) await login('testEr') const deleteResultEr = await send<{ id: number }>('region/2', 'DELETE') + expect(deleteResultEr.body).toEqual(noPermError) expect(deleteResultEr.status).toEqual(403) await login('testEu') const deleteResultEu = await send<{ id: number }>('region/2', 'DELETE') + expect(deleteResultEu.body).toEqual(noPermError) expect(deleteResultEu.status).toEqual(403) await login() diff --git a/backend/src/api-tests/region/get.test.ts b/backend/src/api-tests/region/get.test.ts index 78994239..e5624e85 100644 --- a/backend/src/api-tests/region/get.test.ts +++ b/backend/src/api-tests/region/get.test.ts @@ -1,5 +1,5 @@ import { beforeAll, afterAll, describe, it, expect, beforeEach } from '@jest/globals' -import { login, logout, resetDatabase, send, resetDatabaseTimeout } from '../utils' +import { login, logout, resetDatabase, send, resetDatabaseTimeout, noPermError } from '../utils' import { pool } from '../../utils/db' describe('Getting region data works', () => { @@ -18,17 +18,37 @@ describe('Getting region data works', () => { expect(getResultSu.status).toEqual(200) }) + it('Getting a list of all regions fails without permissions', async () => { + logout() + const getResultNoPerm = await send('region/all', 'GET') + expect(getResultNoPerm.body).toEqual(noPermError) + expect(getResultNoPerm.status).toEqual(403) + + await login('testEr') + const getResultEr = await send('region/all', 'GET') + expect(getResultEr.body).toEqual(noPermError) + expect(getResultEr.status).toEqual(403) + + await login('testEu') + const getResultEu = await send('region/all', 'GET') + expect(getResultEu.body).toEqual(noPermError) + expect(getResultEu.status).toEqual(403) + }) + it('Getting region data fails without permissions', async () => { logout() const getResultNoPerm = await send('region/2', 'GET') + expect(getResultNoPerm.body).toEqual(noPermError) expect(getResultNoPerm.status).toEqual(403) await login('testEr') const getResultEr = await send('region/2', 'GET') + expect(getResultEr.body).toEqual(noPermError) expect(getResultEr.status).toEqual(403) await login('testEu') const getResultEu = await send('region/2', 'GET') + expect(getResultEu.body).toEqual(noPermError) expect(getResultEu.status).toEqual(403) }) }) diff --git a/backend/src/api-tests/sequence.test.ts b/backend/src/api-tests/sequence.test.ts index 0181fc2e..188cf698 100644 --- a/backend/src/api-tests/sequence.test.ts +++ b/backend/src/api-tests/sequence.test.ts @@ -1,5 +1,6 @@ -import { beforeAll, describe, expect, it } from '@jest/globals' +import { beforeAll, afterAll, describe, expect, it } from '@jest/globals' import { login, resetDatabase, resetDatabaseTimeout, send } from './utils' +import { pool } from '../utils/db' type SequenceResponse = { rows: Array<{ sequence: string; seq_name: string; display_value: string }> @@ -14,6 +15,10 @@ describe('Sequence API', () => { await login() }, resetDatabaseTimeout) + afterAll(async () => { + await pool.end() + }) + it('returns sequences with display labels', async () => { const { body, status } = await send('time-unit/sequences', 'GET') diff --git a/backend/src/api-tests/species/merge.test.ts b/backend/src/api-tests/species/merge.test.ts index f5138589..c7c40575 100644 --- a/backend/src/api-tests/species/merge.test.ts +++ b/backend/src/api-tests/species/merge.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals' import { pool } from '../../utils/db' -import { login, noPermError, resetDatabaseTimeout, send, setToken } from '../utils' +import { login, logout, noPermError, resetDatabase, resetDatabaseTimeout, send } from '../utils' import { cloneSpeciesData } from './data' import { OCCURRENCE_MERGE_FIELDS } from '../../services/speciesMerge' import { SpeciesDetailsType } from '../../../../frontend/src/shared/types' @@ -14,6 +14,14 @@ type MergeResponse = { comment: string } +const buildSpeciesPayload = (overrides: Partial> = {}) => { + return { + ...cloneSpeciesData(), + now_ls: [], + ...overrides, + } +} + const buildOccurrenceChoices = ( lid: number, overrides?: Partial> @@ -37,19 +45,11 @@ const buildOccurrenceChoices = ( describe('Species merge endpoint', () => { beforeAll(async () => { - const resetResult = await send('test/reset-test-database', 'GET') - expect(resetResult.status).toEqual(200) - const createUsersResult = await send('test/create-test-users', 'GET') - expect(createUsersResult.status).toEqual(200) - const loginResult = await send<{ token: string }>('user/login', 'POST', { username: 'testSu', password: 'test' }) - expect(loginResult.status).toEqual(200) - setToken(loginResult.body.token) + await resetDatabase() }, resetDatabaseTimeout) beforeEach(async () => { - const loginResult = await send<{ token: string }>('user/login', 'POST', { username: 'testSu', password: 'test' }) - expect(loginResult.status).toEqual(200) - setToken(loginResult.body.token) + await login() }) afterAll(async () => { @@ -59,11 +59,9 @@ describe('Species merge endpoint', () => { it('merges species, migrates synonyms, and deletes obsolete species', async () => { const references = cloneSpeciesData().references - const acceptedPayload = { - ...cloneSpeciesData(), + const acceptedPayload = buildSpeciesPayload({ genus_name: 'MergeAcceptedGenus', species_name: 'mergeAccepted', - unique_identifier: 'merge-accepted', diet1: 'a', now_ls: [ { @@ -75,15 +73,11 @@ describe('Species merge endpoint', () => { }, ], com_taxa_synonym: [], - comment: 'create accepted', - references, - } + }) - const obsoletePayload = { - ...cloneSpeciesData(), + const obsoletePayload = buildSpeciesPayload({ genus_name: 'MergeObsoleteGenus', species_name: 'mergeObsolete', - unique_identifier: 'merge-obsolete', diet1: 'b', now_ls: [ { @@ -102,9 +96,7 @@ describe('Species merge endpoint', () => { syn_comment: 'legacy synonym', }, ], - comment: 'create obsolete', - references, - } + }) const acceptedCreate = await send<{ species_id: number }>('species', 'PUT', { species: acceptedPayload }) const obsoleteCreate = await send<{ species_id: number }>('species', 'PUT', { species: obsoletePayload }) @@ -164,29 +156,12 @@ describe('Species merge endpoint', () => { }) it('requires references', async () => { - const references = cloneSpeciesData().references const acceptedCreate = await send<{ species_id: number }>('species', 'PUT', { - species: { - ...cloneSpeciesData(), - genus_name: 'RefAcceptGenus', - species_name: 'refAccept', - unique_identifier: 'ref-accept', - now_ls: [], - comment: 'create accepted', - references, - }, + species: buildSpeciesPayload({ genus_name: 'RefAcceptGenus' }), }) const obsoleteCreate = await send<{ species_id: number }>('species', 'PUT', { - species: { - ...cloneSpeciesData(), - genus_name: 'RefObsoleteGenus', - species_name: 'refObsolete', - unique_identifier: 'ref-obsolete', - now_ls: [], - comment: 'create obsolete', - references, - }, + species: buildSpeciesPayload({ genus_name: 'RefObsoleteGenus' }), }) const mergeResult = await send('admin/species-merge', 'POST', { @@ -208,32 +183,12 @@ describe('Species merge endpoint', () => { }) it('summary includes non-taxonomic fields and omits taxonomic fields', async () => { - const references = cloneSpeciesData().references - const acceptedCreate = await send<{ species_id: number }>('species', 'PUT', { - species: { - ...cloneSpeciesData(), - genus_name: 'SummaryAcceptGenus', - species_name: 'summaryAccept', - unique_identifier: 'summary-accept', - diet_description: 'accept diet description', - now_ls: [], - comment: 'create accepted', - references, - }, + species: buildSpeciesPayload({ genus_name: 'SummaryAcceptGenus' }), }) const obsoleteCreate = await send<{ species_id: number }>('species', 'PUT', { - species: { - ...cloneSpeciesData(), - genus_name: 'SummaryObsoleteGenus', - species_name: 'summaryObsolete', - unique_identifier: 'summary-obsolete', - diet_description: 'obsolete diet description', - now_ls: [], - comment: 'create obsolete', - references, - }, + species: buildSpeciesPayload({ genus_name: 'SummaryObsoleteGenus' }), }) const summaryResult = await send<{ @@ -256,29 +211,13 @@ describe('Species merge endpoint', () => { const references = cloneSpeciesData().references const acceptedCreate = await send<{ species_id: number }>('species', 'PUT', { - species: { - ...cloneSpeciesData(), - genus_name: 'BoolAcceptGenus', - species_name: 'boolAccept', - unique_identifier: 'bool-accept', - body_mass: 10, - now_ls: [], - comment: 'create accepted', - references, - }, + //@ts-expect-error body_mass is expected to be a BigInt + species: buildSpeciesPayload({ genus_name: 'BoolAcceptGenus', body_mass: 10 }), }) const obsoleteCreate = await send<{ species_id: number }>('species', 'PUT', { - species: { - ...cloneSpeciesData(), - genus_name: 'BoolObsoleteGenus', - species_name: 'boolObsolete', - unique_identifier: 'bool-obsolete', - body_mass: 99, - now_ls: [], - comment: 'create obsolete', - references, - }, + //@ts-expect-error body_mass is expected to be a BigInt + species: buildSpeciesPayload({ genus_name: 'BoolObsoleteGenus', body_mass: 99 }), }) const acceptedId = acceptedCreate.body.species_id @@ -307,19 +246,60 @@ describe('Species merge endpoint', () => { }) it('rejects non-admin users', async () => { - await login('testEr') - const result = await send('admin/species-merge', 'POST', { - obsoleteSpeciesId: 1, - acceptedSpeciesId: 2, + const acceptedCreateResult = await send<{ species_id: number }>('species', 'PUT', { + species: buildSpeciesPayload({ genus_name: 'UserRightTestAcceptedGenus' }), + }) + expect(acceptedCreateResult.status).toEqual(200) + + const obsoleteCreateResult = await send<{ species_id: number }>('species', 'PUT', { + species: buildSpeciesPayload({ genus_name: 'UserRightTestObsoleteGenus' }), + }) + expect(obsoleteCreateResult.status).toEqual(200) + + const acceptedId = acceptedCreateResult.body.species_id + const obsoleteId = obsoleteCreateResult.body.species_id + const references = cloneSpeciesData().references + + logout() + const anonymousUserResult = await send('admin/species-merge', 'POST', { + obsoleteSpeciesId: obsoleteId, + acceptedSpeciesId: acceptedId, selectedSpeciesFieldValues: {}, occurrenceFieldChoices: [], addObsoleteAsSynonym: false, addSourceNameToOccurrences: false, comment: 'merge species', - references: [], + references, }) + expect(anonymousUserResult.body).toEqual(noPermError) + expect(anonymousUserResult.status).toEqual(403) - expect(result.status).toEqual(403) - expect(result.body).toEqual(noPermError) + await login('testEr') + const erResult = await send('admin/species-merge', 'POST', { + obsoleteSpeciesId: obsoleteId, + acceptedSpeciesId: acceptedId, + selectedSpeciesFieldValues: {}, + occurrenceFieldChoices: [], + addObsoleteAsSynonym: false, + addSourceNameToOccurrences: false, + comment: 'merge species', + references, + }) + expect(erResult.status).toEqual(403) + expect(erResult.body).toEqual(noPermError) + + await login('testEu') + const euResult = await send('admin/species-merge', 'POST', { + obsoleteSpeciesId: obsoleteId, + acceptedSpeciesId: acceptedId, + selectedSpeciesFieldValues: {}, + occurrenceFieldChoices: [], + addObsoleteAsSynonym: false, + addSourceNameToOccurrences: false, + comment: 'merge species', + references, + }) + expect(euResult.status).toEqual(403) + expect(euResult.body).toEqual(noPermError) }) }) diff --git a/backend/src/api-tests/timeBound/create.test.ts b/backend/src/api-tests/timeBound/create.test.ts index ba3e8772..dc1e0f4f 100644 --- a/backend/src/api-tests/timeBound/create.test.ts +++ b/backend/src/api-tests/timeBound/create.test.ts @@ -51,6 +51,22 @@ describe('Creating new time bound works', () => { expect(resultWithRef.status).toEqual(200) }) + it('Update logs are correct', () => { + const lastUpdate = createdTimeBound!.now_bau[createdTimeBound!.now_bau.length - 1] + expect(lastUpdate.bau_comment).toEqual(newTimeBoundBasis.comment) // 'Comment is correct' + const logRows = lastUpdate.updates + const expectedLogRows: Partial[] = [ + { + table: 'now_tu_bound', + column: 'b_name', + oldValue: null, + value: newTimeBoundBasis.b_name!, + type: 'add', + }, + ] + testLogRows(logRows, expectedLogRows, 3) + }) + it('Creation fails without permissions', async () => { logout() const resultNoPerm = await send('time-bound', 'PUT', { @@ -67,20 +83,4 @@ describe('Creating new time bound works', () => { expect(resultEr.body).toEqual(noPermError) expect(resultEr.status).toEqual(403) }) - - it('Update logs are correct', () => { - const lastUpdate = createdTimeBound!.now_bau[createdTimeBound!.now_bau.length - 1] - expect(lastUpdate.bau_comment).toEqual(newTimeBoundBasis.comment) // 'Comment is correct' - const logRows = lastUpdate.updates - const expectedLogRows: Partial[] = [ - { - table: 'now_tu_bound', - column: 'b_name', - oldValue: null, - value: newTimeBoundBasis.b_name!, - type: 'add', - }, - ] - testLogRows(logRows, expectedLogRows, 3) - }) }) diff --git a/backend/src/api-tests/timeBound/delete.test.ts b/backend/src/api-tests/timeBound/delete.test.ts index 1fe8a0b7..340fe85b 100644 --- a/backend/src/api-tests/timeBound/delete.test.ts +++ b/backend/src/api-tests/timeBound/delete.test.ts @@ -1,5 +1,5 @@ import { beforeAll, beforeEach, afterAll, describe, it, expect } from '@jest/globals' -import { login, logout, send, resetDatabase, resetDatabaseTimeout } from '../utils' +import { login, logout, send, resetDatabase, resetDatabaseTimeout, noPermError } from '../utils' import { pool } from '../../utils/db' import { newTimeBoundBasis } from './data' @@ -37,10 +37,12 @@ describe('Deleting a time bound', () => { it('without permissions fails', async () => { logout() const deleteResultNoPerm = await send<{ id: number }>('time-bound/11', 'DELETE') + expect(deleteResultNoPerm.body).toEqual(noPermError) expect(deleteResultNoPerm.status).toEqual(403) await login('testEr') const deleteResultEr = await send<{ id: number }>('time-bound/11', 'DELETE') + expect(deleteResultEr.body).toEqual(noPermError) expect(deleteResultEr.status).toEqual(403) }) }) diff --git a/backend/src/api-tests/timeUnit/create.test.ts b/backend/src/api-tests/timeUnit/create.test.ts index b6d09795..e8c4149d 100644 --- a/backend/src/api-tests/timeUnit/create.test.ts +++ b/backend/src/api-tests/timeUnit/create.test.ts @@ -128,23 +128,6 @@ describe('Creating new time unit', () => { }) }) - it('Creation fails without permissions', async () => { - logout() - const resultNoPerm = await send('time-unit', 'PUT', { - timeUnit: { ...newTimeUnitBasis }, - }) - expect(resultNoPerm.body).toEqual(noPermError) - expect(resultNoPerm.status).toEqual(403) - - logout() - await login('testEr', 'test') - const resultEr = await send('time-unit', 'PUT', { - timeUnit: { ...newTimeUnitBasis }, - }) - expect(resultEr.body).toEqual(noPermError) - expect(resultEr.status).toEqual(403) - }) - it('Creation fails without reference', async () => { const resultNoRef = await send('time-unit', 'PUT', { timeUnit: { ...newTimeUnitBasis, tu_display_name: 'Bahean Test 2', references: [] }, @@ -234,5 +217,22 @@ describe('Creating new time unit', () => { expect(getReqStatus).not.toEqual(500) expect(hasStructuredErrorPayload(resultBody)).toBe(true) }) + + it('Creation fails without permissions', async () => { + logout() + const resultNoPerm = await send('time-unit', 'PUT', { + timeUnit: { ...newTimeUnitBasis }, + }) + expect(resultNoPerm.body).toEqual(noPermError) + expect(resultNoPerm.status).toEqual(403) + + logout() + await login('testEr', 'test') + const resultEr = await send('time-unit', 'PUT', { + timeUnit: { ...newTimeUnitBasis }, + }) + expect(resultEr.body).toEqual(noPermError) + expect(resultEr.status).toEqual(403) + }) }) }) diff --git a/backend/src/api-tests/timeUnit/delete.test.ts b/backend/src/api-tests/timeUnit/delete.test.ts index 5f3ef81f..69391a39 100644 --- a/backend/src/api-tests/timeUnit/delete.test.ts +++ b/backend/src/api-tests/timeUnit/delete.test.ts @@ -1,5 +1,5 @@ import { beforeAll, beforeEach, afterAll, describe, it, expect } from '@jest/globals' -import { login, logout, send, resetDatabase, resetDatabaseTimeout } from '../utils' +import { login, logout, send, resetDatabase, resetDatabaseTimeout, noPermError } from '../utils' import { pool } from '../../utils/db' import { newTimeUnitBasis } from './data' import { TIME_UNIT_IN_USE_MESSAGE } from '../../services/write/timeUnit' @@ -41,12 +41,14 @@ describe('Deleting a time unit', () => { it('without permissions fails', async () => { logout() const deleteResultNoPerm = await send<{ id: number }>('time-unit/bahean', 'DELETE') + expect(deleteResultNoPerm.body).toEqual(noPermError) expect(deleteResultNoPerm.status).toEqual(403) const getResultNoPerm = await send('time-unit/bahean', 'GET') expect(getResultNoPerm.status).toEqual(200) await login('testEr') const deleteResultEr = await send<{ id: number }>('time-unit/bahean', 'DELETE') + expect(deleteResultEr.body).toEqual(noPermError) expect(deleteResultEr.status).toEqual(403) const getResultEr = await send('time-unit/bahean', 'GET') expect(getResultEr.status).toEqual(200) diff --git a/backend/src/services/locality.ts b/backend/src/services/locality.ts index c6fac3a2..6c0e9516 100644 --- a/backend/src/services/locality.ts +++ b/backend/src/services/locality.ts @@ -75,13 +75,19 @@ const getActiveProjectIds = (locality: EditDataType) => { export const canEditRestrictedWriteLocality = async (locality: EditDataType, user: User) => { const userProjectIds = await getIdsOfUsersProjects(user) - let activeProjectIds = getActiveProjectIds(locality) - if (activeProjectIds.length === 0 && locality.lid) { - const existingLocality = await getLocalityDetails(locality.lid, user) + let activeProjectIds: number[] = [] + if (locality.lid) { + // this ensures that a user without edit rights cannot simply add the needed projects to the locality while editing it + const existingLocality = await nowDb.now_loc.findUnique({ + where: { lid: locality.lid }, + select: { now_plr: { select: { pid: true } } }, + }) if (existingLocality) { activeProjectIds = existingLocality.now_plr.map(link => link.pid) } + } else { + activeProjectIds = getActiveProjectIds(locality) } if (activeProjectIds.length === 0) return false diff --git a/documentation/testing.md b/documentation/testing.md index 35c1ce9c..fb748bee 100644 --- a/documentation/testing.md +++ b/documentation/testing.md @@ -74,3 +74,8 @@ Your feedback is essential to improving the user experience—thank you for part - The Cypress env variable `databaseResetUrl` points to `http://localhost:4000/test/reset-test-database` by default. Hitting this endpoint (for example with `curl http://localhost:4000/test/reset-test-database`) resets seeded data before running the suite. - When running inside Docker, the provided scripts already pass through the defaults; override with `CYPRESS_databaseResetUrl` if the API host/port differs. +## Jest API Tests + +See [commands.md](devops/commands.md) for commands to run API tests. + +NOTE: When writing API tests, make sure to test that the error messages of failed requests are correct, along with the status code. Both invalid user rights and incorrect requests (invalid or missing values when creating entities etc.) result in error code 403, which might be misleading if only the code is checked. \ No newline at end of file diff --git a/documentation/user_rights.md b/documentation/user_rights.md index 036c7615..41bd11d1 100644 --- a/documentation/user_rights.md +++ b/documentation/user_rights.md @@ -3,6 +3,7 @@ This document shows what each user role can and cannot do. Previously, there were 7 different user roles: + - Admin (su) - EditUnrestricted (eu) - EditRestricted (er) @@ -12,6 +13,7 @@ Previously, there were 7 different user roles: - ReadOnly (ro) In the new UI platform, there are 4 user roles: + - Admin (su) - EditUnrestricted (eu) - EditRestricted (er, pl, plp, no) @@ -42,6 +44,15 @@ Users with the **EditUnrestricted** role are not limited by project membership w - Deleting a species synonym requires the **Admin**, **EditUnrestricted**, or **EditRestricted** roles. - Merging species requires the **Admin** role (via `/admin/species-merge`). +## Occurrence (Cross-Search) + +- Any user can get a list of occurrences. +- Any user can view the details of an occurrence. +- Creating or updating an occurrence requires the **Admin**, **EditUnrestricted**, or **EditRestricted** roles. +- Deleting an occurrence is not possible. + +NOTE: Users with the **EditRestricted** role can only update occurrences that relate to a locality created by that user. Otherwise, they have reading rights only. + ## References - Any user can get a list of References. @@ -74,7 +85,9 @@ Users with the **EditUnrestricted** role are not limited by project membership w ## Person - Viewing a list of every person requires the **Admin** role. -- Any user can get their information using their ID, but cannot view other people's information (unless they have the **Admin** role) +- Any user can get their own information using their ID, but cannot view other people's information (unless they have the **Admin** role) +- Any user can update their own information, but cannot update anyone else's (unless they have the **Admin** role) +- Deleting a Person requires the **Admin** role. ## Project @@ -99,7 +112,6 @@ Users with the **EditUnrestricted** role are not limited by project membership w ## User Rights Table - | User group | Locality | Species | References | Time Units | Time Bounds | Regions | Persons | Projects | Museums | Sedimentary Structures | Sending Email | | ---------------- | -------- | ------- | ---------- | ---------- | ----------- | ------- | ------- | -------- | ------- | ---------------------- | ------------- | | Admin | ALL | ALL | ALL | ALL | ALL | ALL | ALL | ALL | ALL | ALL | C |