From 072183ee552992055fc509195707614feb964e50 Mon Sep 17 00:00:00 2001 From: Steven Weathers Date: Sun, 22 Sep 2024 17:21:38 -0400 Subject: [PATCH] Team endpoint middleware refactor (#625) * Add db method to get user team role info * Update teamUserOnly middleware to get team user role info with new query * Update teamAdminOnly middleware to chain off teamUserOnly middleware * Add e2e seeder class in prep for adding e2e tests for middleware changes * Add teamUserOnly middleware unit tests * format go * Add unit tests for teamAdminOnly middleware --- e2e/fixtures/db/admin-user.ts | 32 +- e2e/fixtures/db/adminapi-user.ts | 50 +- e2e/fixtures/db/api-user.ts | 45 +- e2e/fixtures/db/registered-delete-user.ts | 33 +- e2e/fixtures/db/registered-user.ts | 33 +- e2e/fixtures/db/seeder.ts | 207 +++++++++ e2e/fixtures/db/setup.ts | 19 +- e2e/fixtures/db/verified-user.ts | 37 +- e2e/fixtures/{ => pages}/checkin-page.ts | 0 e2e/fixtures/{ => pages}/department-page.ts | 0 e2e/fixtures/{ => pages}/login-page.ts | 0 e2e/fixtures/{ => pages}/organization-page.ts | 0 e2e/fixtures/{ => pages}/poker-game-page.ts | 0 e2e/fixtures/{ => pages}/poker-games-page.ts | 0 e2e/fixtures/{ => pages}/profile-page.ts | 0 e2e/fixtures/{ => pages}/register-page.ts | 0 e2e/fixtures/{ => pages}/retro-page.ts | 0 e2e/fixtures/{ => pages}/retros-page.ts | 0 e2e/fixtures/{ => pages}/storyboard-page.ts | 0 e2e/fixtures/{ => pages}/storyboards-page.ts | 0 e2e/fixtures/{ => pages}/team-page.ts | 0 e2e/fixtures/{ => pages}/teams-page.ts | 0 e2e/fixtures/test-setup.ts | 90 ++++ e2e/fixtures/types.ts | 17 + e2e/global-setup.ts | 277 ++++++++++- e2e/package-lock.json | 355 ++++++++++++++- e2e/package.json | 7 +- e2e/tests/api/departments.spec.ts | 367 +++++++++++---- e2e/tests/api/dept-teams.spec.ts | 359 +++++++++++++++ e2e/tests/api/org-teams.spec.ts | 260 +++++++++++ e2e/tests/api/organizations.spec.ts | 233 +++++++--- e2e/tests/api/poker-games.spec.ts | 223 +++++---- e2e/tests/api/retros.spec.ts | 201 ++++---- e2e/tests/api/storyboards.spec.ts | 185 ++++---- e2e/tests/api/teams.spec.ts | 324 ++++++++++--- e2e/tests/api/user.spec.ts | 90 ++-- e2e/tests/checkin.spec.ts | 2 +- e2e/tests/department.spec.ts | 2 +- e2e/tests/login.spec.ts | 4 +- e2e/tests/organization.spec.ts | 2 +- e2e/tests/poker-game.spec.ts | 2 +- e2e/tests/poker-games.spec.ts | 2 +- e2e/tests/profile.spec.ts | 2 +- e2e/tests/register.spec.ts | 2 +- e2e/tests/retro.spec.ts | 2 +- e2e/tests/retros.spec.ts | 2 +- e2e/tests/storyboard.spec.ts | 2 +- e2e/tests/storyboards.spec.ts | 2 +- e2e/tests/team.spec.ts | 2 +- e2e/tests/teams.spec.ts | 2 +- e2e/tsconfig.json | 26 ++ go.mod | 10 +- go.sum | 3 +- internal/cookie/auth.go | 6 +- internal/cookie/cookie.go | 10 +- internal/cookie/types.go | 2 +- internal/cookie/user.go | 18 +- internal/db/team/organization.go | 8 +- internal/db/team/user.go | 72 +++ internal/http/auth.go | 2 +- internal/http/checkin/checkin.go | 30 +- internal/http/department.go | 23 +- internal/http/http.go | 166 +++---- internal/http/middleware.go | 269 ++--------- internal/http/middleware_test.go | 429 ++++++++++++++++++ internal/http/organization.go | 24 +- internal/http/poker/poker.go | 8 +- internal/http/retro/retro.go | 8 +- internal/http/storyboard/storyboard.go | 8 +- internal/http/team.go | 18 +- internal/http/types.go | 173 ++++++- internal/http/util.go | 27 +- internal/http/util_test.go | 1 + internal/oauth/oauth.go | 6 +- internal/oauth/type.go | 29 +- thunderdome/admin.go | 6 - thunderdome/alert.go | 9 - thunderdome/apikey.go | 10 - thunderdome/auth.go | 21 - thunderdome/checkin.go | 13 - thunderdome/jira.go | 9 - thunderdome/organization.go | 44 -- thunderdome/team.go | 39 +- 83 files changed, 3657 insertions(+), 1344 deletions(-) create mode 100644 e2e/fixtures/db/seeder.ts rename e2e/fixtures/{ => pages}/checkin-page.ts (100%) rename e2e/fixtures/{ => pages}/department-page.ts (100%) rename e2e/fixtures/{ => pages}/login-page.ts (100%) rename e2e/fixtures/{ => pages}/organization-page.ts (100%) rename e2e/fixtures/{ => pages}/poker-game-page.ts (100%) rename e2e/fixtures/{ => pages}/poker-games-page.ts (100%) rename e2e/fixtures/{ => pages}/profile-page.ts (100%) rename e2e/fixtures/{ => pages}/register-page.ts (100%) rename e2e/fixtures/{ => pages}/retro-page.ts (100%) rename e2e/fixtures/{ => pages}/retros-page.ts (100%) rename e2e/fixtures/{ => pages}/storyboard-page.ts (100%) rename e2e/fixtures/{ => pages}/storyboards-page.ts (100%) rename e2e/fixtures/{ => pages}/team-page.ts (100%) rename e2e/fixtures/{ => pages}/teams-page.ts (100%) create mode 100644 e2e/fixtures/test-setup.ts create mode 100644 e2e/fixtures/types.ts create mode 100644 e2e/tests/api/dept-teams.spec.ts create mode 100644 e2e/tests/api/org-teams.spec.ts create mode 100644 e2e/tsconfig.json create mode 100644 internal/http/middleware_test.go diff --git a/e2e/fixtures/db/admin-user.ts b/e2e/fixtures/db/admin-user.ts index 5aefa0a2..00ae953d 100644 --- a/e2e/fixtures/db/admin-user.ts +++ b/e2e/fixtures/db/admin-user.ts @@ -1,22 +1,20 @@ +import { ThunderdomeSeeder } from "./seeder"; + const adminUser = { name: "E2E ADMIN", email: "e2eadmin@thunderdome.dev", - password: "kentRules!", - hashedPass: "$2a$10$3CvuzyoGIme3dJ4v9BnvyOIKFxEaYyjV2Lfunykv0VokGf/twxi9m", - rank: "ADMIN", + type: "ADMIN", }; const seed = async (pool) => { - const newUser = await pool.query( - `SELECT userid, verifyid FROM thunderdome.user_register($1, $2, $3, $4);`, - [adminUser.name, adminUser.email, adminUser.hashedPass, adminUser.rank], + const seeder = new ThunderdomeSeeder(pool); + const { id } = await seeder.createUser( + adminUser.name, + adminUser.email, + adminUser.type, + true, ); - await pool.query("call thunderdome.user_account_verify($1);", [ - newUser.rows[0].verifyid, - ]); - const id = newUser.rows[0].userid; - return { ...adminUser, id, @@ -24,16 +22,8 @@ const seed = async (pool) => { }; const teardown = async (pool) => { - const oldUser = await pool.query( - `SELECT id FROM thunderdome.users WHERE email = $1;`, - [adminUser.email], - ); - - if (oldUser.rows.length) { - await pool.query("DELETE FROM thunderdome.users WHERE id = $1;", [ - oldUser.rows[0].id, - ]); - } + const seeder = new ThunderdomeSeeder(pool); + await seeder.deleteUserByEmail(adminUser.email); return {}; }; diff --git a/e2e/fixtures/db/adminapi-user.ts b/e2e/fixtures/db/adminapi-user.ts index 72de78c8..a45612f7 100644 --- a/e2e/fixtures/db/adminapi-user.ts +++ b/e2e/fixtures/db/adminapi-user.ts @@ -1,35 +1,25 @@ +import { ThunderdomeSeeder } from "./seeder"; + export const adminAPIUser = { - name: "E2E Admin API User", + name: "E2EAdminAPIUser", email: "e2eadminapi@thunderdome.dev", - password: "kentRules!", - hashedPass: "$2a$10$3CvuzyoGIme3dJ4v9BnvyOIKFxEaYyjV2Lfunykv0VokGf/twxi9m", - rank: "ADMIN", + type: "ADMIN", apikey: "Gssy-ffy.okeTA-3AJhCnY1sqeUvRPRHiNYIVUxs4", }; const seed = async (pool) => { - const newUser = await pool.query( - `SELECT userid, verifyid FROM thunderdome.user_register($1, $2, $3, $4);`, - [ - adminAPIUser.name, - adminAPIUser.email, - adminAPIUser.hashedPass, - adminAPIUser.rank, - ], + const seeder = new ThunderdomeSeeder(pool); + const { id } = await seeder.createUser( + adminAPIUser.name, + adminAPIUser.email, + adminAPIUser.type, + true, ); - const id = newUser.rows[0].userid; - - await pool.query("call thunderdome.user_account_verify($1);", [ - newUser.rows[0].verifyid, - ]); - await pool.query( - `INSERT INTO thunderdome.api_key (id, user_id, name, active) VALUES ($1, $2, $3, TRUE);`, - [ - "Gssy-ffy.e170ffced2ae5806aebc103f30255dc5cc1b9e203d6035aa817f2b7e6638f223", - id, - "test api key 2", - ], + await seeder.addUserAPIKey( + id, + "Gssy-ffy.e170ffced2ae5806aebc103f30255dc5cc1b9e203d6035aa817f2b7e6638f223", + "test api key 2", ); return { @@ -39,16 +29,8 @@ const seed = async (pool) => { }; const teardown = async (pool) => { - const oldUser = await pool.query( - `SELECT id FROM thunderdome.users WHERE email = $1;`, - [adminAPIUser.email], - ); - - if (oldUser.rows.length) { - await pool.query("DELETE FROM thunderdome.users WHERE id = $1;", [ - oldUser.rows[0].id, - ]); - } + const seeder = new ThunderdomeSeeder(pool); + await seeder.deleteUserByEmail(adminAPIUser.email); return {}; }; diff --git a/e2e/fixtures/db/api-user.ts b/e2e/fixtures/db/api-user.ts index 002d48af..42c6abcf 100644 --- a/e2e/fixtures/db/api-user.ts +++ b/e2e/fixtures/db/api-user.ts @@ -1,30 +1,25 @@ +import { ThunderdomeSeeder } from "./seeder"; + export const apiUser = { - name: "E2E API User", + name: "E2EAPIUser", email: "e2eapi@thunderdome.dev", - password: "kentRules!", - hashedPass: "$2a$10$3CvuzyoGIme3dJ4v9BnvyOIKFxEaYyjV2Lfunykv0VokGf/twxi9m", - rank: "REGISTERED", + type: "REGISTERED", apikey: "8MenPkY8.Vqvkh030vv7$rSyYs1gt++L0v7wKuVgR", }; const seed = async (pool) => { - const newUser = await pool.query( - `SELECT userid, verifyid FROM thunderdome.user_register($1, $2, $3, $4);`, - [apiUser.name, apiUser.email, apiUser.hashedPass, apiUser.rank], + const seeder = new ThunderdomeSeeder(pool); + const { id } = await seeder.createUser( + apiUser.name, + apiUser.email, + apiUser.type, + true, ); - const id = newUser.rows[0].userid; - - await pool.query("call thunderdome.user_account_verify($1);", [ - newUser.rows[0].verifyid, - ]); - await pool.query( - `INSERT INTO thunderdome.api_key (id, user_id, name, active) VALUES ($1, $2, $3, TRUE);`, - [ - "8MenPkY8.cd737cbc4bdca1838bdcf1685b00a9a778261255c10193714d9ba1630b55b63c", - id, - "test apikey", - ], + await seeder.addUserAPIKey( + id, + "8MenPkY8.cd737cbc4bdca1838bdcf1685b00a9a778261255c10193714d9ba1630b55b63c", + "test apikey", ); return { @@ -34,16 +29,8 @@ const seed = async (pool) => { }; const teardown = async (pool) => { - const oldUser = await pool.query( - `SELECT id FROM thunderdome.users WHERE email = $1;`, - [apiUser.email], - ); - - if (oldUser.rows.length) { - await pool.query("DELETE FROM thunderdome.users WHERE id = $1;", [ - oldUser.rows[0].id, - ]); - } + const seeder = new ThunderdomeSeeder(pool); + await seeder.deleteUserByEmail(apiUser.email); return {}; }; diff --git a/e2e/fixtures/db/registered-delete-user.ts b/e2e/fixtures/db/registered-delete-user.ts index b2dfeb6b..1ad82dd8 100644 --- a/e2e/fixtures/db/registered-delete-user.ts +++ b/e2e/fixtures/db/registered-delete-user.ts @@ -1,22 +1,19 @@ +import { ThunderdomeSeeder } from "./seeder"; + export const registeredDeleteUser = { name: "E2E Delete User", email: "e2edelete@thunderdome.dev", - password: "kentRules!", - hashedPass: "$2a$10$3CvuzyoGIme3dJ4v9BnvyOIKFxEaYyjV2Lfunykv0VokGf/twxi9m", - rank: "REGISTERED", + type: "REGISTERED", }; const seed = async (pool) => { - const newUser = await pool.query( - `SELECT userid, verifyid FROM thunderdome.user_register($1, $2, $3, $4);`, - [ - registeredDeleteUser.name, - registeredDeleteUser.email, - registeredDeleteUser.hashedPass, - registeredDeleteUser.rank, - ], + const seeder = new ThunderdomeSeeder(pool); + const { id } = await seeder.createUser( + registeredDeleteUser.name, + registeredDeleteUser.email, + registeredDeleteUser.type, + false, ); - const id = newUser.rows[0].userid; return { ...registeredDeleteUser, @@ -25,16 +22,8 @@ const seed = async (pool) => { }; const teardown = async (pool) => { - const oldUser = await pool.query( - `SELECT id FROM thunderdome.users WHERE email = $1;`, - [registeredDeleteUser.email], - ); - - if (oldUser.rows.length) { - await pool.query("DELETE FROM thunderdome.users WHERE id = $1;", [ - oldUser.rows[0].id, - ]); - } + const seeder = new ThunderdomeSeeder(pool); + await seeder.deleteUserByEmail(registeredDeleteUser.email); return {}; }; diff --git a/e2e/fixtures/db/registered-user.ts b/e2e/fixtures/db/registered-user.ts index 95b8fb4c..079762bd 100644 --- a/e2e/fixtures/db/registered-user.ts +++ b/e2e/fixtures/db/registered-user.ts @@ -1,22 +1,19 @@ +import { ThunderdomeSeeder } from "./seeder"; + export const registeredUser = { name: "E2E Registered User", email: "e2eregistered@thunderdome.dev", - password: "kentRules!", - hashedPass: "$2a$10$3CvuzyoGIme3dJ4v9BnvyOIKFxEaYyjV2Lfunykv0VokGf/twxi9m", - rank: "REGISTERED", + type: "REGISTERED", }; const seed = async (pool) => { - const newUser = await pool.query( - `SELECT userid, verifyid FROM thunderdome.user_register($1, $2, $3, $4);`, - [ - registeredUser.name, - registeredUser.email, - registeredUser.hashedPass, - registeredUser.rank, - ], + const seeder = new ThunderdomeSeeder(pool); + const { id } = await seeder.createUser( + registeredUser.name, + registeredUser.email, + registeredUser.type, + false, ); - const id = newUser.rows[0].userid; return { ...registeredUser, @@ -25,16 +22,8 @@ const seed = async (pool) => { }; const teardown = async (pool) => { - const oldUser = await pool.query( - `SELECT id FROM thunderdome.users WHERE email = $1;`, - [registeredUser.email], - ); - - if (oldUser.rows.length) { - await pool.query("DELETE FROM thunderdome.users WHERE id = $1;", [ - oldUser.rows[0].id, - ]); - } + const seeder = new ThunderdomeSeeder(pool); + await seeder.deleteUserByEmail(registeredUser.email); return {}; }; diff --git a/e2e/fixtures/db/seeder.ts b/e2e/fixtures/db/seeder.ts new file mode 100644 index 00000000..8d2d3d84 --- /dev/null +++ b/e2e/fixtures/db/seeder.ts @@ -0,0 +1,207 @@ +import crypto from "crypto"; + +export class ThunderdomeSeeder { + constructor(pool) { + this.pool = pool; + } + + generateEmail(name) { + const sanitizedName = name.toLowerCase().replace(/\s+/g, ""); + const randomString = crypto.randomBytes(4).toString("hex"); + return `${sanitizedName}.${randomString}@thunderometest.com`; + } + + async createUser(name, email, type = "REGISTERED", verified = false) { + const hashedPass = + "$2a$10$3CvuzyoGIme3dJ4v9BnvyOIKFxEaYyjV2Lfunykv0VokGf/twxi9m"; // kentRules! + const { rows } = await this.pool.query( + ` + INSERT INTO thunderdome.users (name, email, type, verified) + VALUES ($1, $2, $3, $4) + RETURNING id + `, + [name, email, type, verified], + ); + const id = rows[0].id; + + await this.pool.query( + `INSERT INTO thunderdome.auth_credential (user_id, email, password, verified) VALUES ($1, $2, $3, $4);`, + [id, email, hashedPass, verified], + ); + + return { + id, + name, + email, + type, + verified, + }; + } + + async addUserAPIKey(userId, apkId, name, active = true) { + await this.pool.query( + ` + INSERT INTO thunderdome.api_key (id, user_id, name, active) VALUES ($1, $2, $3, $4); + `, + [apkId, userId, name, active], + ); + + return { + id: apkId, + name, + active, + }; + } + + async deleteUserByName(name) { + const { rows } = await this.pool.query( + ` + DELETE FROM thunderdome.users + WHERE name = $1 + RETURNING id + `, + [name], + ); + return rows.length > 0 ? rows[0].id : null; + } + + async deleteUserByEmail(email) { + const { rows } = await this.pool.query( + ` + DELETE FROM thunderdome.users + WHERE email = $1 + RETURNING id + `, + [email], + ); + return rows.length > 0 ? rows[0].id : null; + } + + async createOrganization(name, ownerId) { + const result = await this.pool.query( + ` + INSERT INTO thunderdome.organization (name) + VALUES ($1) + RETURNING id + `, + [name], + ); + const orgId = result.rows[0].id; + await this.pool.query( + ` + INSERT INTO thunderdome.organization_user (organization_id, user_id, role) + VALUES ($1, $2, 'ADMIN') + `, + [orgId, ownerId], + ); + return { + id: orgId, + name, + }; + } + + async getOrganizationById(id) { + const { rows } = await this.pool.query( + ` + SELECT id, name, created_date, updated_date FROM thunderdome.organization WHERE id = $1; + `, + [id], + ); + + return rows.length > 0 ? rows[0] : null; + } + + async createDepartment(name, orgId) { + const result = await this.pool.query( + ` + INSERT INTO thunderdome.organization_department (organization_id, name) + VALUES ($1, $2) + RETURNING id + `, + [orgId, name], + ); + return { + id: result.rows[0].id, + organization_id: orgId, + name, + }; + } + + async getDepartmentById(id) { + const { rows } = await this.pool.query( + ` + SELECT id, organization_id, name, created_date, updated_date FROM thunderdome.organization_department WHERE id = $1; + `, + [id], + ); + + return rows.length > 0 ? rows[0] : null; + } + + async createOrgTeam(name, orgId = null, deptId = null) { + const result = await this.pool.query( + ` + INSERT INTO thunderdome.team (name, organization_id, department_id) + VALUES ($1, $2, $3) + RETURNING id + `, + [name, orgId, deptId], + ); + return { + id: result.rows[0].id, + name, + }; + } + + async createUserTeam(name, userId) { + const { rows } = await this.pool.query( + ` + INSERT INTO thunderdome.team (name) + VALUES ($1) + RETURNING id + `, + [name], + ); + const id = rows[0].id; + + await this.addUserToTeam(userId, id, "ADMIN"); + return { + id, + name, + }; + } + + async addUserToOrg(userId, orgId, role = "MEMBER") { + await this.pool.query( + ` + INSERT INTO thunderdome.organization_user (organization_id, user_id, role) + VALUES ($1, $2, $3) + `, + [orgId, userId, role], + ); + } + + async addUserToDept(userId, deptId, role = "MEMBER") { + await this.pool.query( + ` + INSERT INTO thunderdome.department_user (department_id, user_id, role) + VALUES ($1, $2, $3) + `, + [deptId, userId, role], + ); + } + + async addUserToTeam(userId, teamId, role = "MEMBER") { + await this.pool.query( + ` + INSERT INTO thunderdome.team_user (team_id, user_id, role) + VALUES ($1, $2, $3) + `, + [teamId, userId, role], + ); + } + + async close() { + await this.pool.end(); + } +} diff --git a/e2e/fixtures/db/setup.ts b/e2e/fixtures/db/setup.ts index d8a70149..d2d27932 100644 --- a/e2e/fixtures/db/setup.ts +++ b/e2e/fixtures/db/setup.ts @@ -1,11 +1,14 @@ import { Pool } from "pg"; +import process from "process"; -export const setupDB = function (db) { - return new Pool({ - user: db.user, - host: db.host, - database: db.name, - password: db.pass, - port: db.port, - }); +export const setupDB = function () { + const dbConfig = { + database: process.env.DB_NAME || "thunderdome", + user: process.env.DB_USER || "thor", + password: process.env.DB_PASS || "odinson", + port: process.env.DB_PORT || "5432", + host: process.env.DB_HOST || "localhost", + }; + + return new Pool(dbConfig); }; diff --git a/e2e/fixtures/db/verified-user.ts b/e2e/fixtures/db/verified-user.ts index 75a4b1fe..b9f1f78f 100644 --- a/e2e/fixtures/db/verified-user.ts +++ b/e2e/fixtures/db/verified-user.ts @@ -1,27 +1,20 @@ +import { ThunderdomeSeeder } from "./seeder"; + const verifiedUser = { name: "E2E Verified User", email: "e2everified@thunderdome.dev", - password: "kentRules!", - hashedPass: "$2a$10$3CvuzyoGIme3dJ4v9BnvyOIKFxEaYyjV2Lfunykv0VokGf/twxi9m", - rank: "REGISTERED", + type: "REGISTERED", }; const seed = async (pool) => { - const newUser = await pool.query( - `SELECT userid, verifyid FROM thunderdome.user_register($1, $2, $3, $4);`, - [ - verifiedUser.name, - verifiedUser.email, - verifiedUser.hashedPass, - verifiedUser.rank, - ], + const seeder = new ThunderdomeSeeder(pool); + const { id } = await seeder.createUser( + verifiedUser.name, + verifiedUser.email, + verifiedUser.type, + true, ); - await pool.query("call thunderdome.user_account_verify($1);", [ - newUser.rows[0].verifyid, - ]); - const id = newUser.rows[0].userid; - return { ...verifiedUser, id, @@ -29,16 +22,8 @@ const seed = async (pool) => { }; const teardown = async (pool) => { - const oldUser = await pool.query( - `SELECT id FROM thunderdome.users WHERE email = $1;`, - [verifiedUser.email], - ); - - if (oldUser.rows.length) { - await pool.query("DELETE FROM thunderdome.users WHERE id = $1;", [ - oldUser.rows[0].id, - ]); - } + const seeder = new ThunderdomeSeeder(pool); + await seeder.deleteUserByEmail(verifiedUser.email); return {}; }; diff --git a/e2e/fixtures/checkin-page.ts b/e2e/fixtures/pages/checkin-page.ts similarity index 100% rename from e2e/fixtures/checkin-page.ts rename to e2e/fixtures/pages/checkin-page.ts diff --git a/e2e/fixtures/department-page.ts b/e2e/fixtures/pages/department-page.ts similarity index 100% rename from e2e/fixtures/department-page.ts rename to e2e/fixtures/pages/department-page.ts diff --git a/e2e/fixtures/login-page.ts b/e2e/fixtures/pages/login-page.ts similarity index 100% rename from e2e/fixtures/login-page.ts rename to e2e/fixtures/pages/login-page.ts diff --git a/e2e/fixtures/organization-page.ts b/e2e/fixtures/pages/organization-page.ts similarity index 100% rename from e2e/fixtures/organization-page.ts rename to e2e/fixtures/pages/organization-page.ts diff --git a/e2e/fixtures/poker-game-page.ts b/e2e/fixtures/pages/poker-game-page.ts similarity index 100% rename from e2e/fixtures/poker-game-page.ts rename to e2e/fixtures/pages/poker-game-page.ts diff --git a/e2e/fixtures/poker-games-page.ts b/e2e/fixtures/pages/poker-games-page.ts similarity index 100% rename from e2e/fixtures/poker-games-page.ts rename to e2e/fixtures/pages/poker-games-page.ts diff --git a/e2e/fixtures/profile-page.ts b/e2e/fixtures/pages/profile-page.ts similarity index 100% rename from e2e/fixtures/profile-page.ts rename to e2e/fixtures/pages/profile-page.ts diff --git a/e2e/fixtures/register-page.ts b/e2e/fixtures/pages/register-page.ts similarity index 100% rename from e2e/fixtures/register-page.ts rename to e2e/fixtures/pages/register-page.ts diff --git a/e2e/fixtures/retro-page.ts b/e2e/fixtures/pages/retro-page.ts similarity index 100% rename from e2e/fixtures/retro-page.ts rename to e2e/fixtures/pages/retro-page.ts diff --git a/e2e/fixtures/retros-page.ts b/e2e/fixtures/pages/retros-page.ts similarity index 100% rename from e2e/fixtures/retros-page.ts rename to e2e/fixtures/pages/retros-page.ts diff --git a/e2e/fixtures/storyboard-page.ts b/e2e/fixtures/pages/storyboard-page.ts similarity index 100% rename from e2e/fixtures/storyboard-page.ts rename to e2e/fixtures/pages/storyboard-page.ts diff --git a/e2e/fixtures/storyboards-page.ts b/e2e/fixtures/pages/storyboards-page.ts similarity index 100% rename from e2e/fixtures/storyboards-page.ts rename to e2e/fixtures/pages/storyboards-page.ts diff --git a/e2e/fixtures/team-page.ts b/e2e/fixtures/pages/team-page.ts similarity index 100% rename from e2e/fixtures/team-page.ts rename to e2e/fixtures/pages/team-page.ts diff --git a/e2e/fixtures/teams-page.ts b/e2e/fixtures/pages/teams-page.ts similarity index 100% rename from e2e/fixtures/teams-page.ts rename to e2e/fixtures/pages/teams-page.ts diff --git a/e2e/fixtures/test-setup.ts b/e2e/fixtures/test-setup.ts new file mode 100644 index 00000000..f34f57a7 --- /dev/null +++ b/e2e/fixtures/test-setup.ts @@ -0,0 +1,90 @@ +import { APIRequestContext, test as base } from "@playwright/test"; +import { setupDB } from "@fixtures/db/setup"; +import { ThunderdomeSeeder } from "@fixtures/db/seeder"; +import { TestUser, TestUsers } from "@fixtures/types"; + +export type ApiUser = { + context: APIRequestContext; + user: TestUser; +}; + +export type TestDatabase = { + pool: ReturnType; + seeder: ThunderdomeSeeder; +}; + +export type TestFixtures = { + testDatabase: ReturnType; + testUsers: ReturnType; + registeredApiUser: ApiUser; + adminApiUser: ApiUser; + orgOwnerApiUser: ApiUser; + orgAdminApiUser: ApiUser; + orgTeamApiUser: ApiUser; + orgTeamAdminApiUser: ApiUser; + deptAdminApiUser: ApiUser; + deptTeamApiUser: ApiUser; + deptTeamAdminApiUser: ApiUser; + teamAdminApiUser: ApiUser; +}; + +const createApiUserFixture = (userName: keyof TestUsers) => { + return async ( + { + playwright, + testUsers, + baseURL, + }: { playwright: any; testUsers: TestUsers }, + use: (arg: ApiUser) => Promise, + ) => { + const apiUser = testUsers[userName]; + if (!apiUser) { + throw new Error(`${userName} not found in test users data`); + } + const context = await playwright.request.newContext({ + baseURL: `${baseURL}/api/`, + extraHTTPHeaders: { + "X-API-Key": apiUser.apikey, + }, + }); + await use({ context, user: apiUser }); + await context.dispose(); + }; +}; + +export const test = base.extend({ + // Existing testData fixture + testUsers: [ + async ({}, use) => { + // This assumes testData is generated once per worker + // If you need it generated for each test, remove the [, {scope: 'worker'}] part + const data: TestUsers = JSON.parse(process.env.TEST_USERS || "{}"); + await use(data); + }, + { scope: "worker" }, + ], + + // Existing testDatabase fixture + testDatabase: [ + async ({}, use) => { + const pool = setupDB(); + const seeder = new ThunderdomeSeeder(pool); + + await use({ pool, seeder }); + }, + { scope: "worker" }, + ], + + registeredApiUser: createApiUserFixture("registeredUser"), + adminApiUser: createApiUserFixture("adminUser"), + orgOwnerApiUser: createApiUserFixture("orgOwner"), + orgAdminApiUser: createApiUserFixture("orgAdmin"), + orgTeamApiUser: createApiUserFixture("orgTeamMember"), + orgTeamAdminApiUser: createApiUserFixture("orgTeamAdmin"), + deptTeamApiUser: createApiUserFixture("deptTeamMember"), + deptTeamAdminApiUser: createApiUserFixture("deptTeamAdmin"), + deptAdminApiUser: createApiUserFixture("deptAdmin"), + teamAdminApiUser: createApiUserFixture("teamAdmin"), +}); + +export { expect } from "@playwright/test"; diff --git a/e2e/fixtures/types.ts b/e2e/fixtures/types.ts new file mode 100644 index 00000000..55975797 --- /dev/null +++ b/e2e/fixtures/types.ts @@ -0,0 +1,17 @@ +export type TestUser = { + id?: number; + name?: string; + email?: string; + type?: string | "REGISTERED" | "GUEST" | "ADMIN"; + apikey?: string; + verified?: boolean; + orgs?: Array; + orgTeams?: Array; + depts?: Array; + deptTeams?: Array; + teams?: Array; +}; + +export type TestUsers = { + [key: string]: TestUser; +}; diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index d04ae14b..f07be94b 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -1,31 +1,260 @@ import { chromium, expect, FullConfig } from "@playwright/test"; -import { RegisterPage } from "./fixtures/register-page"; -import { LoginPage } from "./fixtures/login-page"; -import { setupDB } from "./fixtures/db/setup"; -import { setupAdminUser } from "./fixtures/db/admin-user"; -import { setupRegisteredUser } from "./fixtures/db/registered-user"; -import { setupDeleteRegisteredUser } from "./fixtures/db/registered-delete-user"; -import { setupVerifiedUser } from "./fixtures/db/verified-user"; -import { setupAPIUser } from "./fixtures/db/api-user"; -import { setupAdminAPIUser } from "./fixtures/db/adminapi-user"; +import { RegisterPage } from "@fixtures/pages/register-page"; +import { LoginPage } from "@fixtures/pages/login-page"; +import { setupDB } from "@fixtures/db/setup"; +import { TestUsers } from "@fixtures/types"; +import { setupAdminUser } from "@fixtures/db/admin-user"; +import { setupRegisteredUser } from "@fixtures/db/registered-user"; +import { setupDeleteRegisteredUser } from "@fixtures/db/registered-delete-user"; +import { setupVerifiedUser } from "@fixtures/db/verified-user"; +import { setupAPIUser } from "@fixtures/db/api-user"; +import { setupAdminAPIUser } from "@fixtures/db/adminapi-user"; +import { ThunderdomeSeeder } from "@fixtures/db/seeder"; +import * as process from "process"; async function globalSetup(config: FullConfig) { - const pool = setupDB({ - name: process.env.DB_NAME || "thunderdome", - user: process.env.DB_USER || "thor", - pass: process.env.DB_PASS || "odinson", - port: process.env.DB_PORT || "5432", - host: process.env.DB_HOST || "localhost", - }); + const pool = setupDB(); + const seeder = new ThunderdomeSeeder(pool); + const testUsers: TestUsers = {}; + + // Create an organization owner with a department and teams + // then create users associated to different entities of that users organization and non-org team + + // Seed Org Owner + const orgOwnerUserName = "E2EOrgOwner"; + await seeder.deleteUserByName(orgOwnerUserName); + const orgOwnerUserEmail = seeder.generateEmail(orgOwnerUserName); + const orgOwnerUser = await seeder.createUser( + orgOwnerUserName, + orgOwnerUserEmail, + ); + await seeder.addUserAPIKey( + orgOwnerUser.id, + "fi5gnBQt.a8c02d29f3984bd33b2a80bb88257e92d56886b840d51897211fc5159e75c668", + "E2E API Key", + ); + const org = await seeder.createOrganization( + `${orgOwnerUserName} Org`, + orgOwnerUser.id, + ); + const dept = await seeder.createDepartment( + `${orgOwnerUserName} Org Dept`, + org.id, + ); + const orgTeam = await seeder.createOrgTeam( + `${orgOwnerUserName} Org Team`, + org.id, + ); + const deptTeam = await seeder.createOrgTeam( + `${orgOwnerUserName} Org Dept Team`, + org.id, + dept.id, + ); + const nonOrgTeam = await seeder.createUserTeam( + `${orgOwnerUserName} Non-Org Team`, + orgOwnerUser.id, + ); + testUsers.orgOwner = { + ...orgOwnerUser, + apikey: "fi5gnBQt.iuiU9V2k_GfNmqjgw1d_m_RIoTRA6uKA", + orgs: [org], + orgTeams: [orgTeam], + depts: [dept], + deptTeams: [deptTeam], + teams: [nonOrgTeam], + }; + console.log(`${orgOwnerUserName} seeded successfully`); + + // Seed User associated to org, org team, and non-org team + const orgTeamUserName = "E2EOrgTeamUser1"; + await seeder.deleteUserByName(orgTeamUserName); + const orgTeamUserEmail = seeder.generateEmail(orgTeamUserName); + const orgTeamUser = await seeder.createUser( + orgTeamUserName, + orgTeamUserEmail, + ); + await seeder.addUserAPIKey( + orgTeamUser.id, + "0DIvRK3H.54e51a06bac4e9b3dcf815282b7955f6e8e2a2cfef7b191ef6d596ea88eb6e0a", + "E2E API Key", + ); + await seeder.addUserToOrg(orgTeamUser.id, org.id); + await seeder.addUserToTeam(orgTeamUser.id, orgTeam.id); + await seeder.addUserToTeam(orgTeamUser.id, nonOrgTeam.id); + testUsers.orgTeamMember = { + ...orgTeamUser, + apikey: "0DIvRK3H.arPJj6FmanALShjgRUX0E_nukNXhqRT=", + orgs: [org], + orgTeams: [orgTeam], + depts: [], + deptTeams: [], + teams: [nonOrgTeam], + }; + console.log(`${orgTeamUserName} seeded successfully`); + + // Seed User associated to org, dept, and dept team + const deptTeamUserName = "E2EOrgDeptTeamUser1"; + await seeder.deleteUserByName(deptTeamUserName); + const deptTeamUserEmail = seeder.generateEmail(deptTeamUserName); + const deptTeamUser = await seeder.createUser( + deptTeamUserName, + deptTeamUserEmail, + ); + await seeder.addUserAPIKey( + deptTeamUser.id, + "rSFqCQ_6.24bf955b6f66556171e111a8ce492a0b24aa7dbfbeb6c3fa0ec9a7e66f048763", + "E2E API Key", + ); + await seeder.addUserToOrg(deptTeamUser.id, org.id); + await seeder.addUserToDept(deptTeamUser.id, dept.id); + await seeder.addUserToTeam(deptTeamUser.id, deptTeam.id); + testUsers.deptTeamMember = { + ...deptTeamUser, + apikey: "rSFqCQ_6.3kzJ=ZMHHfS-o69DF!RhMa+iKQPKZ!6!", + orgs: [org], + orgTeams: [], + depts: [dept], + deptTeams: [deptTeam], + teams: [], + }; + console.log(`${deptTeamUserName} seeded successfully`); + + // Seed User associated to org as ADMIN + const orgAdminUserName = "E2EOrgAdmin1"; + await seeder.deleteUserByName(orgAdminUserName); + const orgAdminUserEmail = seeder.generateEmail(orgAdminUserName); + const orgAdminUser = await seeder.createUser( + orgAdminUserName, + orgAdminUserEmail, + ); + await seeder.addUserAPIKey( + orgAdminUser.id, + "Kgc_ujA_.19b8ddc3dcf9af62b91ec00e0f170b2b99ae01392aad9c2fc5140553463219f4", + "E2E API Key", + ); + await seeder.addUserToOrg(orgAdminUser.id, org.id, "ADMIN"); + testUsers.orgAdmin = { + ...orgAdminUser, + apikey: "Kgc_ujA_.E$ZgDdR4uT+Pvl0Qh=-ZGzz0xobbCoad", + orgs: [org], + orgTeams: [], + depts: [], + deptTeams: [], + teams: [], + }; + console.log(`${orgAdminUserName} seeded successfully`); + + // Seed User associated to dept as ADMIN + const deptAdminUserName = "E2EOrgDeptAdmin1"; + await seeder.deleteUserByName(deptAdminUserName); + const deptAdminUserEmail = seeder.generateEmail(deptAdminUserName); + const deptAdminUser = await seeder.createUser( + deptAdminUserName, + deptAdminUserEmail, + ); + await seeder.addUserAPIKey( + deptAdminUser.id, + "+JEDJS1o.b787661dff2eb536eaa99b217ebf6747e4c511a0a5a97def388a67d449f68c65", + "E2E API Key", + ); + await seeder.addUserToOrg(deptAdminUser.id, org.id); + await seeder.addUserToDept(deptAdminUser.id, dept.id, "ADMIN"); + testUsers.deptAdmin = { + ...deptAdminUser, + apikey: "+JEDJS1o.4FNyoRq4-V0TOyt+dGF93xDUGR_HBOwQ", + orgs: [org], + orgTeams: [], + depts: [dept], + deptTeams: [], + teams: [], + }; + console.log(`${deptAdminUserName} seeded successfully`); + + // Seed User associated to team as ADMIN + const teamAdminUserName = "E2ETeamAdmin1"; + await seeder.deleteUserByName(teamAdminUserName); + const teamAdminUserEmail = seeder.generateEmail(teamAdminUserName); + const teamAdminUser = await seeder.createUser( + teamAdminUserName, + teamAdminUserEmail, + ); + await seeder.addUserAPIKey( + teamAdminUser.id, + "WSpBNDzh.19a1813c398a2f56da96ad0567341e779b37d36b85ccc508511331eaee9d9a3c", + "E2E API Key", + ); + await seeder.addUserToTeam(teamAdminUser.id, nonOrgTeam.id, "ADMIN"); + testUsers.teamAdmin = { + ...teamAdminUser, + apikey: "WSpBNDzh.dTkh7e$8w$54WaxybQk9ObZje4$3sY0C", + orgs: [], + orgTeams: [], + depts: [], + deptTeams: [], + teams: [nonOrgTeam], + }; + console.log(`${teamAdminUserName} seeded successfully`); + + // Seed User associated to Org team as ADMIN + const orgTeamAdminName = "E2EOrgTeamAdmin1"; + await seeder.deleteUserByName(orgTeamAdminName); + const orgTeamAdminEmail = seeder.generateEmail(orgTeamAdminName); + const orgTeamAdmin = await seeder.createUser( + orgTeamAdminName, + orgTeamAdminEmail, + ); + await seeder.addUserAPIKey( + orgTeamAdmin.id, + "Q+PSuFMl.74f05388d298e72ad4b20b6c581303e3c3230cb0242376a07e025a8d3bbb4039", + "E2E API Key", + ); + await seeder.addUserToOrg(orgTeamAdmin.id, org.id); + await seeder.addUserToTeam(orgTeamAdmin.id, orgTeam.id, "ADMIN"); + testUsers.orgTeamAdmin = { + ...orgTeamAdmin, + apikey: "Q+PSuFMl.DRWrKdk82WkaxSmCCntSe35NrVsqYqMj", + orgs: [org], + orgTeams: [orgTeam], + depts: [], + deptTeams: [], + teams: [], + }; + console.log(`${orgTeamAdminName} seeded successfully`); + + const deptTeamAdminUserName = "E2EOrgDeptTeamAdmin1"; + await seeder.deleteUserByName(deptTeamAdminUserName); + const deptTeamAdminUserEmail = seeder.generateEmail(deptTeamAdminUserName); + const deptTeamAdminUser = await seeder.createUser( + deptTeamAdminUserName, + deptTeamAdminUserEmail, + ); + await seeder.addUserAPIKey( + deptTeamAdminUser.id, + "s8GQVacj.566b998dd2b0966bef4092fdda4a5d40b68ddcca46860ed512ce34d2a0acef59", + "E2E API Key", + ); + await seeder.addUserToOrg(deptTeamAdminUser.id, org.id); + await seeder.addUserToDept(deptTeamAdminUser.id, dept.id); + await seeder.addUserToTeam(deptTeamAdminUser.id, deptTeam.id, "ADMIN"); + testUsers.deptTeamAdmin = { + ...deptTeamAdminUser, + apikey: "s8GQVacj.osP4_y8GsGmGgkfq8-T!FAjjDDYMISM$", + orgs: [org], + orgTeams: [], + depts: [dept], + deptTeams: [deptTeam], + teams: [], + }; + console.log(`${deptTeamAdminUserName} seeded successfully`); const baseUrl = config.projects[0].use.baseURL; const browser = await chromium.launch(); await setupAdminAPIUser.teardown(pool); - await setupAdminAPIUser.seed(pool); + testUsers.adminUser = await setupAdminAPIUser.seed(pool); await setupAPIUser.teardown(pool); - await setupAPIUser.seed(pool); + testUsers.registeredUser = await setupAPIUser.seed(pool); const adminPage = await browser.newPage({ baseURL: baseUrl, @@ -34,7 +263,7 @@ async function globalSetup(config: FullConfig) { const au = await setupAdminUser.seed(pool); const adminLoginPage = new LoginPage(adminPage); await adminLoginPage.goto(); - await adminLoginPage.login(au.email, au.password); + await adminLoginPage.login(au.email, "kentRules!"); await expect(adminLoginPage.page.locator("h1")).toHaveText("My Games"); await adminLoginPage.page .context() @@ -47,7 +276,7 @@ async function globalSetup(config: FullConfig) { const ru = await setupRegisteredUser.seed(pool); const registeredRegisterPage = new LoginPage(registeredPage); await registeredRegisterPage.goto(); - await registeredRegisterPage.login(ru.email, ru.password); + await registeredRegisterPage.login(ru.email, "kentRules!"); await expect(registeredRegisterPage.page.locator("h1")).toHaveText( "My Games", ); @@ -62,7 +291,7 @@ async function globalSetup(config: FullConfig) { const vu = await setupVerifiedUser.seed(pool); const userVerifiedPage = new LoginPage(verifiedPage); await userVerifiedPage.goto(); - await userVerifiedPage.login(vu.email, vu.password); + await userVerifiedPage.login(vu.email, "kentRules!"); await expect(userVerifiedPage.page.locator("h1")).toHaveText("My Games"); await userVerifiedPage.page .context() @@ -99,13 +328,17 @@ async function globalSetup(config: FullConfig) { const dru = await setupDeleteRegisteredUser.seed(pool); const deleteRegisteredPage = new LoginPage(deleteRegPage); await deleteRegisteredPage.goto(); - await deleteRegisteredPage.login(dru.email, dru.password); + await deleteRegisteredPage.login(dru.email, "kentRules!"); await expect(deleteRegisteredPage.page.locator("h1")).toHaveText("My Games"); await deleteRegisteredPage.page .context() .storageState({ path: "storage/deleteRegisteredStorageState.json" }); await browser.close(); + + process.env.TEST_USERS = JSON.stringify(testUsers); + + console.log("All users seeded successfully"); } export default globalSetup; diff --git a/e2e/package-lock.json b/e2e/package-lock.json index cde3cfec..90856296 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -12,7 +12,8 @@ "@estruyf/github-actions-reporter": "^1.6.0", "@playwright/test": "^1.47.1", "pg": "^8.11.0", - "prettier": "^3.0.3" + "prettier": "^3.0.3", + "ts-node": "^10.9.2" } }, "node_modules/@actions/core": { @@ -35,6 +36,18 @@ "undici": "^5.25.4" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@estruyf/github-actions-reporter": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@estruyf/github-actions-reporter/-/github-actions-reporter-1.6.0.tgz", @@ -62,6 +75,31 @@ "node": ">=14" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@playwright/test": { "version": "1.47.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz", @@ -77,6 +115,64 @@ "node": ">=18" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "dev": true, + "peer": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ansi-to-html": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", @@ -92,6 +188,12 @@ "node": ">=8.0.0" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/buffer-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", @@ -101,6 +203,21 @@ "node": ">=4" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -124,6 +241,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/marked": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.1.tgz", @@ -326,6 +449,49 @@ "node": ">= 10.x" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -335,6 +501,20 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici": { "version": "5.28.4", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", @@ -347,6 +527,13 @@ "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "peer": true + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -356,6 +543,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -364,6 +557,15 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } } }, "dependencies": { @@ -387,6 +589,15 @@ "undici": "^5.25.4" } }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, "@estruyf/github-actions-reporter": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@estruyf/github-actions-reporter/-/github-actions-reporter-1.6.0.tgz", @@ -404,6 +615,28 @@ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dev": true }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@playwright/test": { "version": "1.47.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz", @@ -413,6 +646,55 @@ "playwright": "1.47.1" } }, + "@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "@types/node": { + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "dev": true, + "peer": true, + "requires": { + "undici-types": "~6.19.2" + } + }, + "acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true + }, + "acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "requires": { + "acorn": "^8.11.0" + } + }, "ansi-to-html": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", @@ -422,12 +704,30 @@ "entities": "^2.2.0" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "buffer-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", "dev": true }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -441,6 +741,12 @@ "dev": true, "optional": true }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "marked": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.1.tgz", @@ -578,12 +884,40 @@ "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", "dev": true }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, "tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true }, + "typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "peer": true + }, "undici": { "version": "5.28.4", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", @@ -593,17 +927,36 @@ "@fastify/busboy": "^2.0.0" } }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "peer": true + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/e2e/package.json b/e2e/package.json index e8c6b7a8..7b6abb24 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -7,7 +7,9 @@ "test": "npx playwright test", "prettier": "prettier --check '**/*.ts'", "prettier:fix": "prettier --write '**/*.ts'", - "format": "npm run prettier:fix" + "format": "npm run prettier:fix", + "test:api": "npx playwright test --grep \"@api\"", + "test:ui": "npx playwright test --grep-invert \"@api\"" }, "author": "Steven Weathers", "license": "Apache-2.0", @@ -15,6 +17,7 @@ "@estruyf/github-actions-reporter": "^1.6.0", "@playwright/test": "^1.47.1", "pg": "^8.11.0", - "prettier": "^3.0.3" + "prettier": "^3.0.3", + "ts-node": "^10.9.2" } } diff --git a/e2e/tests/api/departments.spec.ts b/e2e/tests/api/departments.spec.ts index 83ca20de..32f85c2c 100644 --- a/e2e/tests/api/departments.spec.ts +++ b/e2e/tests/api/departments.spec.ts @@ -1,116 +1,293 @@ -import { expect, test } from "@playwright/test"; -import { adminAPIUser } from "../../fixtures/db/adminapi-user"; -import { apiUser } from "../../fixtures/db/api-user"; -import { baseUrl } from "../../playwright.config"; - -const baseURL = `${baseUrl}/api/`; -const userProfileEndpoint = `auth/user`; - -// Request context is reused by all tests in the file. -let apiContext; -let adminApiContext; -let adminUser; -let user; - -test.beforeAll(async ({ playwright }) => { - apiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": apiUser.apikey, - }, - }); - adminApiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": adminAPIUser.apikey, - }, - }); - const au = await adminApiContext.get(userProfileEndpoint); - const auj = await au.json(); - adminUser = auj.data; - const u = await apiContext.get(userProfileEndpoint); - const uj = await u.json(); - user = uj.data; -}); - -test.afterAll(async ({}) => { - // Dispose all responses. - await apiContext.dispose(); -}); +import { expect, test } from "@fixtures/test-setup"; test.describe( - "registered user", - { tag: ["@api", "@department", "@registered"] }, + "Organization Department API", + { tag: ["@api", "@department"] }, () => { - test(`GET /organizations/{orgId}/departments should return empty array when no departments associated to user`, async () => { - const o = await adminApiContext.post(`users/${user.id}/organizations`, { - data: { - name: "Test API Create Organization", - }, + test.describe("GET /organizations/{orgId}/departments", () => { + test("returns departments list for org member", async ({ + request, + orgTeamApiUser, + }) => { + const org = orgTeamApiUser.user.orgs[0]; + const response = await orgTeamApiUser.context.get( + `organizations/${org.id}/departments`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const departments = await response.json(); + expect(Array.isArray(departments.data)).toBeTruthy(); + expect(departments.data.length).toBeGreaterThan(0); + expect(departments.data[0]).toHaveProperty("id"); + expect(departments.data[0]).toHaveProperty("name"); + }); + + test("returns departments list for org admin", async ({ + request, + orgOwnerApiUser, + }) => { + const org = orgOwnerApiUser.user.orgs[0]; + const response = await orgOwnerApiUser.context.get( + `organizations/${org.id}/departments`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const departments = await response.json(); + expect(Array.isArray(departments.data)).toBeTruthy(); + // Assuming org admin can see all departments, even if there are none + expect(departments.data.length).toBeGreaterThanOrEqual(0); + if (departments.data.length > 0) { + expect(departments.data[0]).toHaveProperty("id"); + expect(departments.data[0]).toHaveProperty("name"); + } + }); + + test("returns departments list for global admin", async ({ + request, + adminApiUser, + orgTeamApiUser, + }) => { + const org = orgTeamApiUser.user.orgs[0]; // Using an org the admin didn't create + const response = await adminApiUser.context.get( + `organizations/${org.id}/departments`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const departments = await response.json(); + expect(Array.isArray(departments.data)).toBeTruthy(); + // Global admin should be able to see all departments, even if there are none + expect(departments.data.length).toBeGreaterThanOrEqual(0); + if (departments.data.length > 0) { + expect(departments.data[0]).toHaveProperty("id"); + expect(departments.data[0]).toHaveProperty("name"); + } + }); + + test("returns 403 Forbidden for non-org member", async ({ + request, + registeredApiUser, + orgTeamApiUser, + }) => { + const org = orgTeamApiUser.user.orgs[0]; // An org the registered user is not part of + const response = await registeredApiUser.context.get( + `organizations/${org.id}/departments`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); }); - const organization = await o.json(); - const b = await adminApiContext.get( - `organizations/${organization.data.id}/departments`, - ); - expect(b.ok()).toBeTruthy(); - const departments = await b.json(); - expect(departments.data).toMatchObject([]); + test("returns empty array when no departments exist", async ({ + request, + orgOwnerApiUser, + testDatabase, + }) => { + // Create a new organization without any departments + const newOrg = await testDatabase.seeder.createOrganization( + "Org Without Departments", + orgOwnerApiUser.user.id, + ); + + const response = await orgOwnerApiUser.context.get( + `organizations/${newOrg.id}/departments`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const departments = await response.json(); + expect(Array.isArray(departments.data)).toBeTruthy(); + expect(departments.data.length).toBe(0); + }); }); - test(`POST /organizations/{orgId}/departments should create department`, async () => { - const o = await apiContext.post(`users/${user.id}/organizations`, { - data: { - name: "Test API Create Organization", - }, + test.describe("POST /organizations/{orgId}/departments", () => { + test("returns 200 for organization admin", async ({ + request, + orgOwnerApiUser, + }) => { + const org = orgOwnerApiUser.user.orgs[0]; + const departmentName = "Test API Create Department Org Admin"; + const response = await orgOwnerApiUser.context.post( + `organizations/${org.id}/departments`, + { + data: { name: departmentName }, + }, + ); + expect(response.ok()).toBeTruthy(); + const department = await response.json(); + expect(department.data).toMatchObject({ name: departmentName }); }); - const organization = await o.json(); - const departmentName = "Test API Create Department"; - - const b = await apiContext.post( - `organizations/${organization.data.id}/departments`, - { - data: { - name: departmentName, + + test("returns 403 for organization member", async ({ + request, + orgOwnerApiUser, + orgTeamApiUser, + }) => { + const org = orgOwnerApiUser.user.orgs[0]; + const departmentName = "Test API Create Department Org Member"; + const response = await orgTeamApiUser.context.post( + `organizations/${org.id}/departments`, + { + data: { name: departmentName }, }, - }, - ); - expect(b.ok()).toBeTruthy(); - const department = await b.json(); - expect(department.data).toMatchObject({ - name: departmentName, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + + test("returns 200 for global admin", async ({ + request, + orgOwnerApiUser, + adminApiUser, + }) => { + const org = orgOwnerApiUser.user.orgs[0]; + const departmentName = "Test API Create Department Global Admin"; + const response = await adminApiUser.context.post( + `organizations/${org.id}/departments`, + { + data: { name: departmentName }, + }, + ); + expect(response.ok()).toBeTruthy(); + const department = await response.json(); + expect(department.data).toMatchObject({ name: departmentName }); }); }); - test(`GET /organizations/{orgId}/departments should return object in array when departments associated to user`, async () => { - const o = await apiContext.post(`users/${user.id}/organizations`, { - data: { - name: "Test API Create Organization", + test.describe("GET /api/organizations/{orgId}/departments/{deptId}", () => { + test("returns 200 and department data for department member", async ({ + request, + deptTeamApiUser, + }) => { + const user = deptTeamApiUser.user; + const dept = user.depts[0]; + const response = await deptTeamApiUser.context.get( + `organizations/${dept.organization_id}/departments/${dept.id}`, + ); + + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const deptData = await response.json(); + expect(deptData.data.department).toMatchObject({ + id: dept.id, + name: dept.name, + }); + }); + + test( + "returns 200 and department data for global admin", + { tag: ["@admin"] }, + async ({ request, adminApiUser, deptTeamApiUser }) => { + const org = deptTeamApiUser.user.orgs[0]; + const dept = deptTeamApiUser.user.depts[0]; + const response = await adminApiUser.context.get( + `organizations/${org.id}/departments/${dept.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const deptData = await response.json(); + expect(deptData.data.department).toMatchObject({ + id: dept.id, + name: dept.name, + }); }, + ); + + test("returns 403 Forbidden for non department member", async ({ + request, + registeredApiUser, + deptTeamApiUser, + }) => { + const org = deptTeamApiUser.user.orgs[0]; + const dept = deptTeamApiUser.user.depts[0]; + const response = await registeredApiUser.context.get( + `organizations/${org.id}/departments/${dept.id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); }); - const organization = await o.json(); - - const departmentName = "Test API Departments"; - const b = await apiContext.post( - `organizations/${organization.data.id}/departments`, - { - data: { - name: departmentName, + }); + + test.describe("PUT /organization/{orgId}/departments/{deptId}", () => { + test("Org Admin can update any department in their organization", async ({ + orgAdminApiUser, + testDatabase, + }) => { + const org = orgAdminApiUser.user.orgs[0]; + const dept = await testDatabase.seeder.createDepartment( + "Dept to Update", + org.id, + ); + const updatedName = "Updated by Org Admin"; + const response = await orgAdminApiUser.context.put( + `organizations/${org.id}/departments/${dept.id}`, + { + data: { name: updatedName }, }, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const updatedDept = await response.json(); + expect(updatedDept.data.name).toBe(updatedName); + }); + }); + + test.describe("DELETE /api/organizations/{orgId}/departments/{deptId}", () => { + test("returns 200 for org admin", async ({ + request, + orgOwnerApiUser, + testDatabase, + }) => { + const org = orgOwnerApiUser.user.orgs[0]; + const dept = await testDatabase.seeder.createDepartment( + "deptToDeleteOrgOwner", + org.id, + ); + + const response = await orgOwnerApiUser.context.delete( + `organizations/${org.id}/departments/${dept.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + + const confirmDeptDeleted = await testDatabase.seeder.getDepartmentById( + dept.id, + ); + expect(confirmDeptDeleted).toBeNull(); + }); + + test( + "returns 200 for global admin", + { tag: ["@admin"] }, + async ({ request, orgOwnerApiUser, adminApiUser, testDatabase }) => { + const org = orgOwnerApiUser.user.orgs[0]; + const dept = await testDatabase.seeder.createDepartment( + "deptToDeleteGlobalAdmin", + org.id, + ); + + const response = await adminApiUser.context.delete( + `organizations/${org.id}/departments/${dept.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + + const confirmDeptDeleted = + await testDatabase.seeder.getDepartmentById(dept.id); + expect(confirmDeptDeleted).toBeNull(); }, ); - expect(b.ok()).toBeTruthy(); - const bs = await apiContext.get( - `organizations/${organization.data.id}/departments`, - ); - expect(bs.ok()).toBeTruthy(); - const departments = await bs.json(); - expect(departments.data).toContainEqual( - expect.objectContaining({ - name: departmentName, - }), - ); + test("returns 403 Forbidden for non org admin", async ({ + request, + deptTeamApiUser, + }) => { + const org = deptTeamApiUser.user.orgs[0]; + const dept = deptTeamApiUser.user.depts[0]; + const response = await deptTeamApiUser.context.delete( + `organizations/${org.id}/departments/${dept.id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); }); }, ); diff --git a/e2e/tests/api/dept-teams.spec.ts b/e2e/tests/api/dept-teams.spec.ts new file mode 100644 index 00000000..e9fd2b8f --- /dev/null +++ b/e2e/tests/api/dept-teams.spec.ts @@ -0,0 +1,359 @@ +import { expect, test } from "@fixtures/test-setup"; + +test.describe( + "Organization Department Team API", + { tag: ["@api", "@department", "@organization"] }, + () => { + test.describe("GET /teams/{teamId}", () => { + test("Department Admin can view any team in their department", async ({ + deptAdminApiUser, + deptTeamApiUser, + }) => { + const departmentTeam = deptTeamApiUser.user.deptTeams[0]; + const response = await deptAdminApiUser.context.get( + `teams/${departmentTeam.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const tr = await response.json(); + expect(tr.data.team.id).toBe(departmentTeam.id); + }); + + test("Department Team Admin can view department team details", async ({ + deptTeamApiUser, + deptTeamAdminApiUser, + }) => { + const departmentTeam = deptTeamApiUser.user.deptTeams[0]; + const getResponse = await deptTeamAdminApiUser.context.get( + `teams/${departmentTeam.id}`, + ); + expect(getResponse.ok()).toBeTruthy(); + expect(getResponse.status()).toBe(200); + const tr = await getResponse.json(); + expect(tr.data).toMatchObject({ + team: { id: departmentTeam.id, name: departmentTeam.name }, + }); + }); + + test("Department Team Member can view department team details", async ({ + deptTeamApiUser, + }) => { + const departmentTeam = deptTeamApiUser.user.deptTeams[0]; + const response = await deptTeamApiUser.context.get( + `teams/${departmentTeam.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const tr = await response.json(); + expect(tr.data.team.id).toBe(departmentTeam.id); + }); + + test("Global Admin can view any department team", async ({ + adminApiUser, + deptTeamApiUser, + }) => { + const departmentTeam = deptTeamApiUser.user.deptTeams[0]; + const response = await adminApiUser.context.get( + `teams/${departmentTeam.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const tr = await response.json(); + expect(tr.data.team.id).toBe(departmentTeam.id); + }); + + test("Non-department team member cannot view department team details", async ({ + registeredApiUser, + deptTeamApiUser, + }) => { + const departmentTeam = deptTeamApiUser.user.deptTeams[0]; + const response = await registeredApiUser.context.get( + `teams/${departmentTeam.id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); + + test.describe("PUT /teams/{teamId}", () => { + test("Department Admin can update any team in their department", async ({ + testDatabase, + deptAdminApiUser, + orgOwnerApiUser, + }) => { + const dept = orgOwnerApiUser.user.depts[0]; + const departmentTeam = await testDatabase.seeder.createOrgTeam( + "departmentTeamToUpdateDeptAdmin", + orgOwnerApiUser.user.orgs[0].id, + dept.id, + ); + const updatedName = "Dept Admin Updated Team"; + const response = await deptAdminApiUser.context.put( + `teams/${departmentTeam.id}`, + { + data: { name: updatedName }, + }, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const updatedDepartmentTeam = await response.json(); + expect(updatedDepartmentTeam.data.name).toBe(updatedName); + }); + + test("Department Admin cannot update team outside their department", async ({ + testDatabase, + deptAdminApiUser, + orgOwnerApiUser, + }) => { + const otherDept = await testDatabase.seeder.createDepartment( + "Other Department", + orgOwnerApiUser.user.orgs[0].id, + ); + const otherDeptTeam = await testDatabase.seeder.createOrgTeam( + "OtherDeptTeam", + orgOwnerApiUser.user.orgs[0].id, + otherDept.id, + ); + const updatedName = "Unauthorized Update"; + const response = await deptAdminApiUser.context.put( + `teams/${otherDeptTeam.id}`, + { + data: { name: updatedName }, + }, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + + test("Department Team Member cannot update department team details", async ({ + deptTeamApiUser, + }) => { + const updatedName = "Unauthorized Update"; + const response = await deptTeamApiUser.context.put( + `teams/${deptTeamApiUser.user.deptTeams[0].id}`, + { + data: { name: updatedName }, + }, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + + test("Department Team Admin can update department team details", async ({ + testDatabase, + deptTeamAdminApiUser, + orgOwnerApiUser, + }) => { + const dept = orgOwnerApiUser.user.depts[0]; + const departmentTeam = await testDatabase.seeder.createOrgTeam( + "departmentTeamToUpdateTeamAdmin", + orgOwnerApiUser.user.orgs[0].id, + dept.id, + ); + await testDatabase.seeder.addUserToTeam( + deptTeamAdminApiUser.user.id, + departmentTeam.id, + "ADMIN", + ); + const updatedName = "Admin Updated Department Team"; + const response = await deptTeamAdminApiUser.context.put( + `teams/${departmentTeam.id}`, + { + data: { name: updatedName }, + }, + ); + // expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const updatedDepartmentTeam = await response.json(); + expect(updatedDepartmentTeam.data.name).toBe(updatedName); + }); + + test("Global Admin can update any department team", async ({ + testDatabase, + adminApiUser, + orgOwnerApiUser, + }) => { + const dept = orgOwnerApiUser.user.depts[0]; + const departmentTeam = await testDatabase.seeder.createOrgTeam( + "departmentTeamToUpdateGlobalAdmin", + orgOwnerApiUser.user.orgs[0].id, + dept.id, + ); + const updatedName = "Global Admin Updated Department Team"; + const response = await adminApiUser.context.put( + `teams/${departmentTeam.id}`, + { + data: { name: updatedName }, + }, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const updatedDepartmentTeam = await response.json(); + expect(updatedDepartmentTeam.data.name).toBe(updatedName); + }); + + test("Non-department team member cannot update department team", async ({ + deptTeamApiUser, + registeredApiUser, + }) => { + const updatedName = "Unauthorized Update"; + const response = await registeredApiUser.context.put( + `teams/${deptTeamApiUser.user.deptTeams[0].id}`, + { + data: { name: updatedName }, + }, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); + + test.describe("DELETE /teams/{teamId}", () => { + test("Department Admin can delete any team in their department", async ({ + testDatabase, + deptAdminApiUser, + orgOwnerApiUser, + }) => { + const dept = orgOwnerApiUser.user.depts[0]; + const departmentTeam = await testDatabase.seeder.createOrgTeam( + "departmentTeamToDeleteDeptAdmin", + orgOwnerApiUser.user.orgs[0].id, + dept.id, + ); + const response = await deptAdminApiUser.context.delete( + `teams/${departmentTeam.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + }); + + test("Department Admin cannot delete team outside their department", async ({ + testDatabase, + deptAdminApiUser, + orgOwnerApiUser, + }) => { + const otherDept = await testDatabase.seeder.createDepartment( + "Other Department2", + orgOwnerApiUser.user.orgs[0].id, + ); + const otherDeptTeam = await testDatabase.seeder.createOrgTeam( + "OtherDeptTeam2", + orgOwnerApiUser.user.orgs[0].id, + otherDept.id, + ); + const response = await deptAdminApiUser.context.delete( + `teams/${otherDeptTeam.id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + + test("Department Team Admin can delete department team", async ({ + testDatabase, + orgOwnerApiUser, + deptTeamAdminApiUser, + }) => { + const dept = orgOwnerApiUser.user.depts[0]; + const departmentTeam = await testDatabase.seeder.createOrgTeam( + "departmentTeamToDeleteTeamAdmin", + orgOwnerApiUser.user.orgs[0].id, + dept.id, + ); + await testDatabase.seeder.addUserToTeam( + deptTeamAdminApiUser.user.id, + departmentTeam.id, + "ADMIN", + ); + const response = await deptTeamAdminApiUser.context.delete( + `teams/${departmentTeam.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + }); + + test("Global Admin can delete any department team", async ({ + testDatabase, + adminApiUser, + orgOwnerApiUser, + }) => { + const dept = orgOwnerApiUser.user.depts[0]; + const departmentTeam = await testDatabase.seeder.createOrgTeam( + "departmentTeamToUpdateTeamAdmin", + orgOwnerApiUser.user.orgs[0].id, + dept.id, + ); + const response = await adminApiUser.context.delete( + `teams/${departmentTeam.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + }); + + test("Department Team Member cannot delete department team", async ({ + deptTeamApiUser, + }) => { + const response = await deptTeamApiUser.context.delete( + `teams/${deptTeamApiUser.user.deptTeams[0].id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + + test("Non-department team member cannot delete department team", async ({ + deptTeamApiUser, + registeredApiUser, + }) => { + const response = await registeredApiUser.context.delete( + `teams/${deptTeamApiUser.user.deptTeams[0].id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); + + test.describe("POST /departments/{departmentId}/teams", () => { + test("Department Admin can create a new team in their department", async ({ + deptAdminApiUser, + orgOwnerApiUser, + }) => { + const dept = orgOwnerApiUser.user.depts[0]; + const newTeamName = "New Team Created by Dept Admin"; + const response = await deptAdminApiUser.context.post( + `organizations/${orgOwnerApiUser.user.orgs[0].id}/departments/${dept.id}/teams`, + { + data: { name: newTeamName }, + }, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const newTeam = await response.json(); + expect(newTeam.data.name).toBe(newTeamName); + }); + }); + + test.describe("GET /departments/{departmentId}/teams", () => { + test("Department Admin can list all teams in their department", async ({ + deptAdminApiUser, + orgOwnerApiUser, + }) => { + const dept = orgOwnerApiUser.user.depts[0]; + const response = await deptAdminApiUser.context.get( + `organizations/${orgOwnerApiUser.user.orgs[0].id}/departments/${dept.id}/teams`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const teams = await response.json(); + expect(Array.isArray(teams.data)).toBeTruthy(); + expect(teams.data.length).toBeGreaterThan(0); + + expect(teams.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: orgOwnerApiUser.user.deptTeams[0].id, + }), + ]), + ); + }); + }); + }, +); diff --git a/e2e/tests/api/org-teams.spec.ts b/e2e/tests/api/org-teams.spec.ts new file mode 100644 index 00000000..87895a70 --- /dev/null +++ b/e2e/tests/api/org-teams.spec.ts @@ -0,0 +1,260 @@ +import { expect, test } from "@fixtures/test-setup"; + +test.describe( + "Organization Team API", + { tag: ["@api", "@team", "@organization"] }, + () => { + test.describe("POST /organizations/{orgId}/teams", () => { + test("Org Admin can create a new team in any department of their organization", async ({ + orgAdminApiUser, + testDatabase, + }) => { + const org = orgAdminApiUser.user.orgs[0]; + const dept = await testDatabase.seeder.createDepartment( + "Dept for New Team", + org.id, + ); + const newTeamName = "New Team Created by Org Admin"; + const response = await orgAdminApiUser.context.post( + `organizations/${org.id}/teams`, + { + data: { name: newTeamName, department_id: dept.id }, + }, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const newTeam = await response.json(); + expect(newTeam.data.name).toBe(newTeamName); + }); + }); + + test.describe("GET /teams/{teamId}", () => { + test("Team Admin can view team details", async ({ + orgTeamApiUser, + orgTeamAdminApiUser, + }) => { + const team = orgTeamApiUser.user.orgTeams[0]; + const getResponse = await orgTeamAdminApiUser.context.get( + `teams/${team.id}`, + ); + expect(getResponse.ok()).toBeTruthy(); + expect(getResponse.status()).toBe(200); + const tr = await getResponse.json(); + expect(tr.data).toMatchObject({ + team: { id: team.id, name: team.name }, + }); + }); + + test("Team Member can view team details", async ({ orgTeamApiUser }) => { + const team = orgTeamApiUser.user.orgTeams[0]; + const response = await orgTeamApiUser.context.get(`teams/${team.id}`); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const tr = await response.json(); + expect(tr.data.team.id).toBe(team.id); + }); + + test("Global Admin can view any team", async ({ + adminApiUser, + orgTeamApiUser, + }) => { + const team = orgTeamApiUser.user.orgTeams[0]; + const response = await adminApiUser.context.get(`teams/${team.id}`); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const tr = await response.json(); + expect(tr.data.team.id).toBe(team.id); + }); + + test("Non-team member cannot view team details", async ({ + registeredApiUser, + orgTeamApiUser, + }) => { + const team = orgTeamApiUser.user.orgTeams[0]; + const response = await registeredApiUser.context.get( + `teams/${team.id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); + + test.describe("PUT /teams/{teamId}", () => { + test("Team Member cannot update team details", async ({ + orgTeamApiUser, + }) => { + const updatedName = "Unauthorized Update"; + const response = await orgTeamApiUser.context.put( + `teams/${orgTeamApiUser.user.orgTeams[0].id}`, + { + data: { name: updatedName }, + }, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + + test("Team Admin can update team details", async ({ + testDatabase, + teamAdminApiUser, + orgOwnerApiUser, + }) => { + const team = await testDatabase.seeder.createOrgTeam( + "teamToUpdateTeamAdmin", + orgOwnerApiUser.user.orgs[1], + ); + await testDatabase.seeder.addUserToTeam( + teamAdminApiUser.user.id, + team.id, + "ADMIN", + ); + const updatedName = "Admin Updated Team"; + const response = await teamAdminApiUser.context.put( + `teams/${team.id}`, + { + data: { name: updatedName }, + }, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const updatedTeam = await response.json(); + expect(updatedTeam.data.name).toBe(updatedName); + }); + + test("Org Admin can update any team in their organization", async ({ + orgAdminApiUser, + testDatabase, + }) => { + const org = orgAdminApiUser.user.orgs[0]; + const dept = await testDatabase.seeder.createDepartment( + "Dept for Team Update", + org.id, + ); + const team = await testDatabase.seeder.createOrgTeam( + "Team to Update", + org.id, + dept.id, + ); + const updatedName = "Updated by Org Admin"; + const response = await orgAdminApiUser.context.put(`teams/${team.id}`, { + data: { name: updatedName }, + }); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const updatedTeam = await response.json(); + expect(updatedTeam.data.name).toBe(updatedName); + }); + + test("Global Admin can update any team", async ({ + testDatabase, + adminApiUser, + orgOwnerApiUser, + }) => { + const team = await testDatabase.seeder.createOrgTeam( + "teamToUpdateGlobalAdmin", + orgOwnerApiUser.user.orgs[1], + ); + const updatedName = "Global Admin Updated Team"; + const response = await adminApiUser.context.put(`teams/${team.id}`, { + data: { name: updatedName }, + }); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const updatedTeam = await response.json(); + expect(updatedTeam.data.name).toBe(updatedName); + }); + + test("Non-team member cannot update team", async ({ + orgTeamApiUser, + registeredApiUser, + }) => { + const updatedName = "Unauthorized Update"; + const response = await registeredApiUser.context.put( + `teams/${orgTeamApiUser.user.orgTeams[0].id}`, + { + data: { name: updatedName }, + }, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); + + test.describe("DELETE /teams/{teamId}", () => { + test("Team Admin can delete team", async ({ + testDatabase, + orgOwnerApiUser, + teamAdminApiUser, + }) => { + const team = await testDatabase.seeder.createOrgTeam( + "teamToDeleteTeamAdmin", + orgOwnerApiUser.user.orgs[0].id, + ); + await testDatabase.seeder.addUserToTeam( + teamAdminApiUser.user.id, + team.id, + "ADMIN", + ); + const response = await teamAdminApiUser.context.delete( + `teams/${team.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + }); + + test("Org Admin can delete any team in their organization", async ({ + orgAdminApiUser, + testDatabase, + }) => { + const org = orgAdminApiUser.user.orgs[0]; + const dept = await testDatabase.seeder.createDepartment( + "Dept for Team Deletion", + org.id, + ); + const team = await testDatabase.seeder.createOrgTeam( + "Team to Delete", + org.id, + dept.id, + ); + const response = await orgAdminApiUser.context.delete( + `teams/${team.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + }); + + test("Global Admin can delete any team", async ({ + testDatabase, + adminApiUser, + orgOwnerApiUser, + }) => { + const team = await testDatabase.seeder.createOrgTeam( + "teamToDeleteGlobalAdmin", + orgOwnerApiUser.user.orgs[0].id, + ); + const response = await adminApiUser.context.delete(`teams/${team.id}`); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + }); + + test("Team Member cannot delete team", async ({ orgTeamApiUser }) => { + const response = await orgTeamApiUser.context.delete( + `teams/${orgTeamApiUser.user.orgTeams[0].id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + + test("Non-team member cannot delete team", async ({ + orgTeamApiUser, + registeredApiUser, + }) => { + const response = await registeredApiUser.context.delete( + `teams/${orgTeamApiUser.user.orgTeams[0].id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); + }, +); diff --git a/e2e/tests/api/organizations.spec.ts b/e2e/tests/api/organizations.spec.ts index 3490a0de..74c602d0 100644 --- a/e2e/tests/api/organizations.spec.ts +++ b/e2e/tests/api/organizations.spec.ts @@ -1,90 +1,181 @@ -import { expect, test } from "@playwright/test"; -import { adminAPIUser } from "../../fixtures/db/adminapi-user"; -import { apiUser } from "../../fixtures/db/api-user"; -import { baseUrl } from "../../playwright.config"; +import { expect, test } from "@fixtures/test-setup"; -const baseURL = `${baseUrl}/api/`; -const userProfileEndpoint = `auth/user`; +test.describe("Organization API", { tag: ["@api", "@organization"] }, () => { + test.describe("GET /users/{userId}/organizations", () => { + test("returns empty array when no organizations associated to user", async ({ + request, + adminApiUser, + }) => { + const response = await adminApiUser.context.get( + `users/${adminApiUser.user.id}/organizations`, + ); + expect(response.ok()).toBeTruthy(); + const organizations = await response.json(); + expect(organizations.data).toEqual([]); + }); -// Request context is reused by all tests in the file. -let apiContext; -let adminApiContext; -let adminUser; -let user; + test("returns object in array when organizations associated to user", async ({ + request, + orgTeamApiUser, + }) => { + const response = await orgTeamApiUser.context.get( + `users/${orgTeamApiUser.user.id}/organizations`, + ); + expect(response.ok()).toBeTruthy(); + const organizations = await response.json(); + expect(organizations.data).toContainEqual( + expect.objectContaining({ + id: orgTeamApiUser.user.orgs[0].id, + name: orgTeamApiUser.user.orgs[0].name, + }), + ); + }); + }); -test.beforeAll(async ({ playwright }) => { - apiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": apiUser.apikey, - }, + test.describe("POST /users/{userId}/organizations", () => { + test("creates organization", async ({ request, registeredApiUser }) => { + const organizationName = "Test API Create Organization"; + const response = await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/organizations`, + { + data: { name: organizationName }, + }, + ); + expect(response.ok()).toBeTruthy(); + const organization = await response.json(); + expect(organization.data).toMatchObject({ name: organizationName }); + }); }); - adminApiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": adminAPIUser.apikey, - }, + + test.describe("GET /api/organizations/{orgId}", () => { + test("returns 200 and organization data for org member", async ({ + request, + orgTeamApiUser, + }) => { + const org = orgTeamApiUser.user.orgs[0]; + const response = await orgTeamApiUser.context.get( + `organizations/${org.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const orgData = await response.json(); + expect(orgData.data.organization).toMatchObject({ + id: org.id, + name: org.name, + }); + }); + + test( + "returns 200 and organization data for global admin", + { tag: ["@admin"] }, + async ({ request, adminApiUser, orgTeamApiUser }) => { + const org = orgTeamApiUser.user.orgs[0]; + const response = await adminApiUser.context.get( + `organizations/${org.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const orgData = await response.json(); + expect(orgData.data.organization).toMatchObject({ + id: org.id, + name: org.name, + }); + }, + ); + + test("returns 403 Forbidden for non-org member", async ({ + request, + registeredApiUser, + orgTeamApiUser, + }) => { + const org = orgTeamApiUser.user.orgs[0]; + const response = await registeredApiUser.context.get( + `organizations/${org.id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); }); - const au = await adminApiContext.get(userProfileEndpoint); - const auj = await au.json(); - adminUser = auj.data; - const u = await apiContext.get(userProfileEndpoint); - const uj = await u.json(); - user = uj.data; -}); -test.afterAll(async ({}) => { - // Dispose all responses. - await apiContext.dispose(); -}); + test.describe("DELETE /api/organizations/{orgId}", () => { + test("returns 200 for org admin", async ({ + request, + orgOwnerApiUser, + testDatabase, + }) => { + const org = await testDatabase.seeder.createOrganization( + "orgToDeleteOwner", + orgOwnerApiUser.user.id, + ); -test.describe( - "registered user", - { tag: ["@api", "@organization", "@registered"] }, - () => { - test(`GET /users/{userId}/organizations should return empty array when no organizations associated to user`, async () => { - const b = await adminApiContext.get( - `users/${adminUser.id}/organizations`, + const response = await orgOwnerApiUser.context.delete( + `organizations/${org.id}`, ); - expect(b.ok()).toBeTruthy(); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); - const organizations = await b.json(); - expect(organizations.data).toMatchObject([]); + const confirmOrgDeleted = await testDatabase.seeder.getOrganizationById( + org.id, + ); + expect(confirmOrgDeleted).toBeNull(); }); - test(`POST /users/{userId}/organizations should create organization`, async () => { - const organizationName = "Test API Create Organization"; + test( + "returns 200 for global admin", + { tag: ["@admin"] }, + async ({ request, orgOwnerApiUser, adminApiUser, testDatabase }) => { + const org = await testDatabase.seeder.createOrganization( + "orgToDeleteGlobalAdmin", + orgOwnerApiUser.user.id, + ); - const b = await apiContext.post(`users/${user.id}/organizations`, { - data: { - name: organizationName, - }, - }); - expect(b.ok()).toBeTruthy(); - const organization = await b.json(); - expect(organization.data).toMatchObject({ - name: organizationName, - }); - }); + const response = await adminApiUser.context.delete( + `organizations/${org.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); - test(`GET /users/{userId}/organizations should return object in array when organizations associated to user`, async () => { - const organizationName = "Test API Organizations"; + const confirmOrgDeleted = await testDatabase.seeder.getOrganizationById( + org.id, + ); + expect(confirmOrgDeleted).toBeNull(); + }, + ); - const b = await apiContext.post(`users/${user.id}/organizations`, { - data: { - name: organizationName, - }, - }); - expect(b.ok()).toBeTruthy(); + test("returns 403 Forbidden for non org admin", async ({ + request, + orgTeamApiUser, + }) => { + const org = orgTeamApiUser.user.orgs[0]; + const response = await orgTeamApiUser.context.delete( + `organizations/${org.id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); - const bs = await apiContext.get(`users/${user.id}/organizations`); - expect(bs.ok()).toBeTruthy(); - const organizations = await bs.json(); - expect(organizations.data).toContainEqual( + test.describe("GET /organizations/{orgId}/teams", () => { + test("Org Admin can list all teams in their organization", async ({ + orgAdminApiUser, + orgOwnerApiUser, + }) => { + const org = orgAdminApiUser.user.orgs[0]; + const response = await orgAdminApiUser.context.get( + `organizations/${org.id}/teams`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const teams = await response.json(); + expect(Array.isArray(teams.data)).toBeTruthy(); + expect(teams.data.length).toBeGreaterThan(0); + + expect(teams.data).toContainEqual( expect.objectContaining({ - name: organizationName, + id: orgOwnerApiUser.user.orgTeams[0].id, + name: orgOwnerApiUser.user.orgTeams[0].name, }), ); }); - }, -); + }); +}); diff --git a/e2e/tests/api/poker-games.spec.ts b/e2e/tests/api/poker-games.spec.ts index efd61fbb..8c7023ec 100644 --- a/e2e/tests/api/poker-games.spec.ts +++ b/e2e/tests/api/poker-games.spec.ts @@ -1,149 +1,132 @@ -import { expect, test } from "@playwright/test"; -import { adminAPIUser } from "../../fixtures/db/adminapi-user"; -import { apiUser } from "../../fixtures/db/api-user"; -import { baseUrl } from "../../playwright.config"; +import { expect, test } from "@fixtures/test-setup"; -const baseURL = `${baseUrl}/api/`; -const userProfileEndpoint = `auth/user`; - -// Request context is reused by all tests in the file. -let apiContext; -let adminApiContext; -let adminUser; -let user; - -test.beforeAll(async ({ playwright }) => { - apiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": apiUser.apikey, - }, - }); - adminApiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": adminAPIUser.apikey, - }, +test.describe("Poker API", { tag: ["@api", "@poker"] }, () => { + test("GET /users/{userId}/battles returns empty array when no games associated to user", async ({ + request, + adminApiUser, + }) => { + const response = await adminApiUser.context.get( + `users/${adminApiUser.user.id}/battles`, + ); + expect(response.ok()).toBeTruthy(); + const battles = await response.json(); + expect(battles.data).toEqual([]); }); - const au = await adminApiContext.get(userProfileEndpoint); - const auj = await au.json(); - adminUser = auj.data; - const u = await apiContext.get(userProfileEndpoint); - const uj = await u.json(); - user = uj.data; -}); - -test.afterAll(async ({}) => { - // Dispose all responses. - await apiContext.dispose(); -}); -test.describe( - "registered user", - { tag: ["@api", "@registered", "@poker"] }, - () => { - test(`GET /users/{userId}/battles should return empty array when no games associated to user`, async () => { - const b = await adminApiContext.get(`users/${adminUser.id}/battles`); - expect(b.ok()).toBeTruthy(); + test("POST /users/{userId}/battles creates game", async ({ + request, + registeredApiUser, + }) => { + const pointValuesAllowed = ["0", "1/2", "1", "2", "3", "5", "8", "13"]; + const battleName = "Test API Create Game"; + const pointAverageRounding = "floor"; + const autoFinishVoting = false; - const battles = await b.json(); - expect(battles.data).toMatchObject([]); + const response = await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/battles`, + { + data: { + name: battleName, + pointValuesAllowed, + pointAverageRounding, + autoFinishVoting, + }, + }, + ); + expect(response.ok()).toBeTruthy(); + const battle = await response.json(); + expect(battle.data).toMatchObject({ + name: battleName, + pointValuesAllowed, + pointAverageRounding, + autoFinishVoting, }); + }); - test(`POST /users/{userId}/battles should create game`, async () => { - const pointValuesAllowed = ["0", "1/2", "1", "2", "3", "5", "8", "13"]; - const battleName = "Test API Create Game"; - const pointAverageRounding = "floor"; - const autoFinishVoting = false; + test("GET /users/{userId}/battles returns object in array when games associated to user", async ({ + request, + registeredApiUser, + }) => { + const pointValuesAllowed = ["1", "2", "3", "5", "8", "13"]; + const battleName = "Test API Games"; + const pointAverageRounding = "ceil"; + const autoFinishVoting = true; - const b = await apiContext.post(`users/${user.id}/battles`, { + await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/battles`, + { data: { name: battleName, pointValuesAllowed, pointAverageRounding, autoFinishVoting, }, - }); - expect(b.ok()).toBeTruthy(); - const battle = await b.json(); - expect(battle.data).toMatchObject({ + }, + ); + + const response = await registeredApiUser.context.get( + `users/${registeredApiUser.user.id}/battles`, + ); + expect(response.ok()).toBeTruthy(); + const battles = await response.json(); + expect(battles.data).toContainEqual( + expect.objectContaining({ name: battleName, pointValuesAllowed, pointAverageRounding, autoFinishVoting, - }); - }); + }), + ); + }); - test(`GET /users/{userId}/battles should return object in array when games associated to user`, async () => { - const pointValuesAllowed = ["1", "2", "3", "5", "8", "13"]; - const battleName = "Test API Games"; - const pointAverageRounding = "ceil"; - const autoFinishVoting = true; + test("POST /teams/{teamId}/users/{userId}/battles creates game", async ({ + request, + registeredApiUser, + }) => { + const pointValuesAllowed = ["0", "1/2", "1", "2", "3", "5", "8", "13"]; + const battleName = "Test API Create Game"; + const pointAverageRounding = "floor"; + const autoFinishVoting = false; - const b = await apiContext.post(`users/${user.id}/battles`, { + const teamResponse = await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/teams`, + { data: { - name: battleName, - pointValuesAllowed, - pointAverageRounding, - autoFinishVoting, + name: "test team create game", }, - }); - expect(b.ok()).toBeTruthy(); + }, + ); + const { data: team } = await teamResponse.json(); - const bs = await apiContext.get(`users/${user.id}/battles`); - expect(bs.ok()).toBeTruthy(); - const battles = await bs.json(); - expect(battles.data).toContainEqual( - expect.objectContaining({ + const battleResponse = await registeredApiUser.context.post( + `teams/${team.id}/users/${registeredApiUser.user.id}/battles`, + { + data: { name: battleName, pointValuesAllowed, pointAverageRounding, autoFinishVoting, - }), - ); - }); - - test(`POST /teams/{teamId}/users/{userId}/battles should create game`, async () => { - const pointValuesAllowed = ["0", "1/2", "1", "2", "3", "5", "8", "13"]; - const battleName = "Test API Create Game"; - const pointAverageRounding = "floor"; - const autoFinishVoting = false; - - const t = await apiContext.post(`users/${user.id}/teams`, { - data: { - name: "test team create game", }, - }); - const { data: team } = await t.json(); + }, + ); + expect(battleResponse.ok()).toBeTruthy(); + const battle = await battleResponse.json(); + expect(battle.data).toMatchObject({ + name: battleName, + pointValuesAllowed, + pointAverageRounding, + autoFinishVoting, + }); - const b = await apiContext.post( - `teams/${team.id}/users/${user.id}/battles`, - { - data: { - name: battleName, - pointValuesAllowed, - pointAverageRounding, - autoFinishVoting, - }, - }, - ); - expect(b.ok()).toBeTruthy(); - const battle = await b.json(); - expect(battle.data).toMatchObject({ + const battlesResponse = await registeredApiUser.context.get( + `teams/${team.id}/battles`, + ); + expect(battlesResponse.ok()).toBeTruthy(); + const battles = await battlesResponse.json(); + expect(battles.data).toContainEqual( + expect.objectContaining({ name: battleName, - pointValuesAllowed, - pointAverageRounding, - autoFinishVoting, - }); - - const bs = await apiContext.get(`teams/${team.id}/battles`); - expect(bs.ok()).toBeTruthy(); - const battles = await bs.json(); - expect(battles.data).toContainEqual( - expect.objectContaining({ - name: battleName, - }), - ); - }); - }, -); + }), + ); + }); +}); diff --git a/e2e/tests/api/retros.spec.ts b/e2e/tests/api/retros.spec.ts index 8c2b5c1b..012c133e 100644 --- a/e2e/tests/api/retros.spec.ts +++ b/e2e/tests/api/retros.spec.ts @@ -1,135 +1,118 @@ -import { expect, test } from "@playwright/test"; -import { adminAPIUser } from "../../fixtures/db/adminapi-user"; -import { apiUser } from "../../fixtures/db/api-user"; -import { baseUrl } from "../../playwright.config"; +import { expect, test } from "@fixtures/test-setup"; -const baseURL = `${baseUrl}/api/`; -const userProfileEndpoint = `auth/user`; - -// Request context is reused by all tests in the file. -let apiContext; -let adminApiContext; -let adminUser; -let user; - -test.beforeAll(async ({ playwright }) => { - apiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": apiUser.apikey, - }, - }); - adminApiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": adminAPIUser.apikey, - }, +test.describe("Retro API", { tag: ["@api", "@retro"] }, () => { + test("GET /users/{userId}/retros returns empty array when no retros associated to user", async ({ + request, + adminApiUser, + }) => { + const response = await adminApiUser.context.get( + `users/${adminApiUser.user.id}/retros`, + ); + expect(response.ok()).toBeTruthy(); + const retros = await response.json(); + expect(retros.data).toEqual([]); }); - const au = await adminApiContext.get(userProfileEndpoint); - const auj = await au.json(); - adminUser = auj.data; - const u = await apiContext.get(userProfileEndpoint); - const uj = await u.json(); - user = uj.data; -}); - -test.afterAll(async ({}) => { - // Dispose all responses. - await apiContext.dispose(); -}); -test.describe( - "registered user", - { tag: ["@api", "@registered", "@retro"] }, - () => { - test(`GET /users/{userId}/retros should return empty array when no retros associated to user`, async () => { - const b = await adminApiContext.get(`users/${adminUser.id}/retros`); - expect(b.ok()).toBeTruthy(); + test("POST /users/{userId}/retros creates retro", async ({ + request, + registeredApiUser, + }) => { + const retroName = "Test API Create Retro"; + const brainstormVisibility = "visible"; + const maxVotes = 3; - const retros = await b.json(); - expect(retros.data).toMatchObject([]); - }); - - test(`POST /users/{userId}/retros should create retro`, async () => { - const retroName = "Test API Create Retro"; - const brainstormVisibility = "visible"; - const maxVotes = 3; - - const b = await apiContext.post(`users/${user.id}/retros`, { + const response = await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/retros`, + { data: { retroName, brainstormVisibility, maxVotes, }, - }); - expect(b.ok()).toBeTruthy(); - const retro = await b.json(); - expect(retro.data).toMatchObject({ - name: retroName, - brainstormVisibility, - }); + }, + ); + expect(response.ok()).toBeTruthy(); + const retro = await response.json(); + expect(retro.data).toMatchObject({ + name: retroName, + brainstormVisibility, }); + }); - test(`GET /users/{userId}/retros should return object in array when retros associated to user`, async () => { - const retroName = "Test API Retros"; - const brainstormVisibility = "hidden"; - const maxVotes = 3; + test("GET /users/{userId}/retros returns object in array when retros associated to user", async ({ + request, + registeredApiUser, + }) => { + const retroName = "Test API Retros"; + const brainstormVisibility = "hidden"; + const maxVotes = 3; - const b = await apiContext.post(`users/${user.id}/retros`, { + await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/retros`, + { data: { retroName, brainstormVisibility, maxVotes, }, - }); - expect(b.ok()).toBeTruthy(); + }, + ); - const bs = await apiContext.get(`users/${user.id}/retros`); - expect(bs.ok()).toBeTruthy(); - const retros = await bs.json(); - expect(retros.data).toContainEqual( - expect.objectContaining({ - name: retroName, - }), - ); - }); + const response = await registeredApiUser.context.get( + `users/${registeredApiUser.user.id}/retros`, + ); + expect(response.ok()).toBeTruthy(); + const retros = await response.json(); + expect(retros.data).toContainEqual( + expect.objectContaining({ + name: retroName, + }), + ); + }); - test(`POST /teams/{teamId}/users/{userId}/retros should create retro`, async () => { - const retroName = "Test API Create Team Retro"; - const brainstormVisibility = "hidden"; - const maxVotes = 3; + test("POST /teams/{teamId}/users/{userId}/retros creates retro", async ({ + request, + registeredApiUser, + }) => { + const retroName = "Test API Create Team Retro"; + const brainstormVisibility = "hidden"; + const maxVotes = 3; - const t = await apiContext.post(`users/${user.id}/teams`, { + const teamResponse = await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/teams`, + { data: { name: "test team create retro", }, - }); - const { data: team } = await t.json(); + }, + ); + const { data: team } = await teamResponse.json(); - const b = await apiContext.post( - `teams/${team.id}/users/${user.id}/retros`, - { - data: { - retroName, - brainstormVisibility, - maxVotes, - }, + const retroResponse = await registeredApiUser.context.post( + `teams/${team.id}/users/${registeredApiUser.user.id}/retros`, + { + data: { + retroName, + brainstormVisibility, + maxVotes, }, - ); - expect(b.ok()).toBeTruthy(); - const retro = await b.json(); - expect(retro.data).toMatchObject({ - name: retroName, - }); - - const bs = await apiContext.get(`teams/${team.id}/retros`); - expect(bs.ok()).toBeTruthy(); - const retros = await bs.json(); - expect(retros.data).toContainEqual( - expect.objectContaining({ - name: retroName, - }), - ); + }, + ); + expect(retroResponse.ok()).toBeTruthy(); + const retro = await retroResponse.json(); + expect(retro.data).toMatchObject({ + name: retroName, }); - }, -); + + const retrosResponse = await registeredApiUser.context.get( + `teams/${team.id}/retros`, + ); + expect(retrosResponse.ok()).toBeTruthy(); + const retros = await retrosResponse.json(); + expect(retros.data).toContainEqual( + expect.objectContaining({ + name: retroName, + }), + ); + }); +}); diff --git a/e2e/tests/api/storyboards.spec.ts b/e2e/tests/api/storyboards.spec.ts index 5eeb6815..7196cee4 100644 --- a/e2e/tests/api/storyboards.spec.ts +++ b/e2e/tests/api/storyboards.spec.ts @@ -1,122 +1,105 @@ -import { expect, test } from "@playwright/test"; -import { adminAPIUser } from "../../fixtures/db/adminapi-user"; -import { apiUser } from "../../fixtures/db/api-user"; -import { baseUrl } from "../../playwright.config"; +import { expect, test } from "@fixtures/test-setup"; -const baseURL = `${baseUrl}/api/`; -const userProfileEndpoint = `auth/user`; - -// Request context is reused by all tests in the file. -let apiContext; -let adminApiContext; -let adminUser; -let user; - -test.beforeAll(async ({ playwright }) => { - apiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": apiUser.apikey, - }, - }); - adminApiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": adminAPIUser.apikey, - }, +test.describe("Storyboard API", { tag: ["@api", "@storyboard"] }, () => { + test("GET /users/{userId}/storyboards returns empty array when no storyboards associated to user", async ({ + request, + adminApiUser, + }) => { + const response = await adminApiUser.context.get( + `users/${adminApiUser.user.id}/storyboards`, + ); + expect(response.ok()).toBeTruthy(); + const storyboards = await response.json(); + expect(storyboards.data).toEqual([]); }); - const au = await adminApiContext.get(userProfileEndpoint); - const auj = await au.json(); - adminUser = auj.data; - const u = await apiContext.get(userProfileEndpoint); - const uj = await u.json(); - user = uj.data; -}); - -test.afterAll(async ({}) => { - // Dispose all responses. - await apiContext.dispose(); -}); -test.describe( - "registered user", - { tag: ["@api", "@registered", "@storyboard"] }, - () => { - test(`GET /users/{userId}/storyboards should return empty array when no storyboards associated to user`, async () => { - const b = await adminApiContext.get(`users/${adminUser.id}/storyboards`); - expect(b.ok()).toBeTruthy(); + test("POST /users/{userId}/storyboards creates storyboard", async ({ + request, + registeredApiUser, + }) => { + const storyboardName = "Test API Create Storyboard"; - const storyboards = await b.json(); - expect(storyboards.data).toMatchObject([]); - }); - - test(`POST /users/{userId}/storyboards should create storyboard`, async () => { - const storyboardName = "Test API Create Storyboard"; - - const b = await apiContext.post(`users/${user.id}/storyboards`, { + const response = await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/storyboards`, + { data: { storyboardName, }, - }); - expect(b.ok()).toBeTruthy(); - const storyboard = await b.json(); - expect(storyboard.data).toMatchObject({ - name: storyboardName, - }); + }, + ); + expect(response.ok()).toBeTruthy(); + const storyboard = await response.json(); + expect(storyboard.data).toMatchObject({ + name: storyboardName, }); + }); - test(`GET /users/{userId}/storyboards should return object in array when storyboards associated to user`, async () => { - const storyboardName = "Test API Storyboards"; + test("GET /users/{userId}/storyboards returns object in array when storyboards associated to user", async ({ + request, + registeredApiUser, + }) => { + const storyboardName = "Test API Storyboards"; - const b = await apiContext.post(`users/${user.id}/storyboards`, { + await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/storyboards`, + { data: { storyboardName, }, - }); - expect(b.ok()).toBeTruthy(); + }, + ); - const bs = await apiContext.get(`users/${user.id}/storyboards`); - expect(bs.ok()).toBeTruthy(); - const storyboards = await bs.json(); - expect(storyboards.data).toContainEqual( - expect.objectContaining({ - name: storyboardName, - }), - ); - }); + const response = await registeredApiUser.context.get( + `users/${registeredApiUser.user.id}/storyboards`, + ); + expect(response.ok()).toBeTruthy(); + const storyboards = await response.json(); + expect(storyboards.data).toContainEqual( + expect.objectContaining({ + name: storyboardName, + }), + ); + }); - test(`POST /teams/{teamId}/users/{userId}/storyboards should create storyboard`, async () => { - const storyboardName = "Test API Create Team Storyboard"; + test("POST /teams/{teamId}/users/{userId}/storyboards creates storyboard", async ({ + request, + registeredApiUser, + }) => { + const storyboardName = "Test API Create Team Storyboard"; - const t = await apiContext.post(`users/${user.id}/teams`, { + const teamResponse = await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/teams`, + { data: { - name: "test team create retro", + name: "test team create storyboard", }, - }); - const { data: team } = await t.json(); + }, + ); + const { data: team } = await teamResponse.json(); - const b = await apiContext.post( - `teams/${team.id}/users/${user.id}/storyboards`, - { - data: { - storyboardName, - }, + const storyboardResponse = await registeredApiUser.context.post( + `teams/${team.id}/users/${registeredApiUser.user.id}/storyboards`, + { + data: { + storyboardName, }, - ); - expect(b.ok()).toBeTruthy(); - const storyboard = await b.json(); - expect(storyboard.data).toMatchObject({ - name: storyboardName, - }); - - const bs = await apiContext.get(`teams/${team.id}/storyboards`); - expect(bs.ok()).toBeTruthy(); - const storyboards = await bs.json(); - expect(storyboards.data).toContainEqual( - expect.objectContaining({ - name: storyboardName, - }), - ); + }, + ); + expect(storyboardResponse.ok()).toBeTruthy(); + const storyboard = await storyboardResponse.json(); + expect(storyboard.data).toMatchObject({ + name: storyboardName, }); - }, -); + + const storyboardsResponse = await registeredApiUser.context.get( + `teams/${team.id}/storyboards`, + ); + expect(storyboardsResponse.ok()).toBeTruthy(); + const storyboards = await storyboardsResponse.json(); + expect(storyboards.data).toContainEqual( + expect.objectContaining({ + name: storyboardName, + }), + ); + }); +}); diff --git a/e2e/tests/api/teams.spec.ts b/e2e/tests/api/teams.spec.ts index 1dfae18c..f3f3af19 100644 --- a/e2e/tests/api/teams.spec.ts +++ b/e2e/tests/api/teams.spec.ts @@ -1,88 +1,270 @@ -import { expect, test } from "@playwright/test"; -import { adminAPIUser } from "../../fixtures/db/adminapi-user"; -import { apiUser } from "../../fixtures/db/api-user"; -import { baseUrl } from "../../playwright.config"; - -const baseURL = `${baseUrl}/api/`; -const userProfileEndpoint = `auth/user`; - -// Request context is reused by all tests in the file. -let apiContext; -let adminApiContext; -let adminUser; -let user; - -test.beforeAll(async ({ playwright }) => { - apiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": apiUser.apikey, - }, +import { expect, test } from "@fixtures/test-setup"; + +test.describe("Team API", { tag: ["@api", "@team"] }, () => { + test.describe("GET /users/{userId}/teams", () => { + test("returns empty array when no teams associated to user for Entity User", async ({ + orgAdminApiUser, + }) => { + const response = await orgAdminApiUser.context.get( + `users/${orgAdminApiUser.user.id}/teams`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const teams = await response.json(); + expect(teams.data).toEqual([]); + }); + + test("returns empty array when no teams associated to user for Global Admin", async ({ + orgAdminApiUser, + adminApiUser, + }) => { + const response = await adminApiUser.context.get( + `users/${orgAdminApiUser.user.id}/teams`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const teams = await response.json(); + expect(teams.data).toEqual([]); + }); + + test("returns object in array when teams associated to user for Entity User", async ({ + orgOwnerApiUser, + }) => { + const team = orgOwnerApiUser.user.teams[0]; + const response = await orgOwnerApiUser.context.get( + `users/${orgOwnerApiUser.user.id}/teams`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const teams = await response.json(); + expect(teams.data).toContainEqual( + expect.objectContaining({ + id: team.id, + name: team.name, + }), + ); + }); + + test("returns object in array when teams associated to user for Global Admin", async ({ + adminApiUser, + orgOwnerApiUser, + }) => { + const team = orgOwnerApiUser.user.teams[0]; + const response = await adminApiUser.context.get( + `users/${orgOwnerApiUser.user.id}/teams`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const teams = await response.json(); + expect(teams.data).toContainEqual( + expect.objectContaining({ + id: team.id, + name: team.name, + }), + ); + }); + + test("returns forbidden for Non Entity User", async ({ + teamAdminApiUser, + registeredApiUser, + }) => { + const response = await registeredApiUser.context.get( + `users/${teamAdminApiUser.user.id}/teams`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); }); - adminApiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": adminAPIUser.apikey, - }, + + test.describe("POST /users/{userId}/teams", () => { + test("creates team", async ({ registeredApiUser }) => { + const teamName = "Test API Create Team"; + const response = await registeredApiUser.context.post( + `users/${registeredApiUser.user.id}/teams`, + { + data: { name: teamName }, + }, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const team = await response.json(); + expect(team.data).toMatchObject({ name: teamName }); + }); }); - const au = await adminApiContext.get(userProfileEndpoint); - const auj = await au.json(); - adminUser = auj.data; - const u = await apiContext.get(userProfileEndpoint); - const uj = await u.json(); - user = uj.data; -}); -test.afterAll(async ({}) => { - // Dispose all responses. - await apiContext.dispose(); -}); + test.describe("GET /teams/{teamId}", () => { + test("Team Admin can view team details", async ({ teamAdminApiUser }) => { + const team = teamAdminApiUser.user.teams[0]; + const getResponse = await teamAdminApiUser.context.get( + `teams/${team.id}`, + ); + expect(getResponse.ok()).toBeTruthy(); + expect(getResponse.status()).toBe(200); + const tr = await getResponse.json(); + expect(tr.data).toMatchObject({ + team: { id: team.id, name: team.name }, + }); + }); -test.describe( - "registered user", - { tag: ["@api", "@registered", "@team"] }, - () => { - test(`GET /users/{userId}/teams should return empty array when no teams associated to user`, async () => { - const b = await adminApiContext.get(`users/${adminUser.id}/teams`); - expect(b.ok()).toBeTruthy(); + test("Team Member can view team details", async ({ orgTeamApiUser }) => { + const team = orgTeamApiUser.user.teams[0]; + const response = await orgTeamApiUser.context.get(`teams/${team.id}`); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const tr = await response.json(); + expect(tr.data.team.id).toBe(team.id); + }); - const teams = await b.json(); - expect(teams.data).toMatchObject([]); + test("Global Admin can view any team", async ({ + adminApiUser, + orgTeamApiUser, + }) => { + const team = orgTeamApiUser.user.teams[0]; + const response = await adminApiUser.context.get(`teams/${team.id}`); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const tr = await response.json(); + expect(tr.data.team.id).toBe(team.id); }); - test(`POST /users/{userId}/teams should create team`, async () => { - const teamName = "Test API Create Team"; + test("Non-team member cannot view team details", async ({ + registeredApiUser, + orgTeamApiUser, + }) => { + const team = orgTeamApiUser.user.teams[0]; + const response = await registeredApiUser.context.get(`teams/${team.id}`); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); - const b = await apiContext.post(`users/${user.id}/teams`, { - data: { - name: teamName, + test.describe("PUT /teams/{teamId}", () => { + test("Team Member cannot update team details", async ({ + orgTeamApiUser, + }) => { + const updatedName = "Unauthorized Update"; + const response = await orgTeamApiUser.context.put( + `teams/${orgTeamApiUser.user.teams[0].id}`, + { + data: { name: updatedName }, }, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + + test("Team Admin can update team details", async ({ + testDatabase, + teamAdminApiUser, + registeredApiUser, + }) => { + const team = await testDatabase.seeder.createUserTeam( + "teamToUpdate", + registeredApiUser.user.id, + ); + await testDatabase.seeder.addUserToTeam( + teamAdminApiUser.user.id, + team.id, + "ADMIN", + ); + const updatedName = "Admin Updated Team"; + const response = await teamAdminApiUser.context.put(`teams/${team.id}`, { + data: { name: updatedName }, }); - expect(b.ok()).toBeTruthy(); - const team = await b.json(); - expect(team.data).toMatchObject({ - name: teamName, - }); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const updatedTeam = await response.json(); + expect(updatedTeam.data.name).toBe(updatedName); }); - test(`GET /users/{userId}/teams should return object in array when teams associated to user`, async () => { - const teamName = "Test API Teams"; + test("Global Admin can update any team", async ({ + testDatabase, + adminApiUser, + registeredApiUser, + }) => { + const team = await testDatabase.seeder.createUserTeam( + "teamToUpdate", + registeredApiUser.user.id, + ); + const updatedName = "Global Admin Updated Team"; + const response = await adminApiUser.context.put(`teams/${team.id}`, { + data: { name: updatedName }, + }); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const updatedTeam = await response.json(); + expect(updatedTeam.data.name).toBe(updatedName); + }); - const b = await apiContext.post(`users/${user.id}/teams`, { - data: { - name: teamName, + test("Non-team member cannot update team", async ({ + orgTeamApiUser, + registeredApiUser, + }) => { + const updatedName = "Unauthorized Update"; + const response = await registeredApiUser.context.put( + `teams/${orgTeamApiUser.user.teams[0].id}`, + { + data: { name: updatedName }, }, - }); - expect(b.ok()).toBeTruthy(); + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); - const bs = await apiContext.get(`users/${user.id}/teams`); - expect(bs.ok()).toBeTruthy(); - const teams = await bs.json(); - expect(teams.data).toContainEqual( - expect.objectContaining({ - name: teamName, - }), + test.describe("DELETE /teams/{teamId}", () => { + test("Team Admin can delete team", async ({ + testDatabase, + registeredApiUser, + teamAdminApiUser, + }) => { + const team = await testDatabase.seeder.createUserTeam( + "teamToUpdate", + registeredApiUser.user.id, + ); + await testDatabase.seeder.addUserToTeam( + teamAdminApiUser.user.id, + team.id, + "ADMIN", + ); + const response = await teamAdminApiUser.context.delete( + `teams/${team.id}`, + ); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + }); + + test("Global Admin can delete any team", async ({ + testDatabase, + adminApiUser, + registeredApiUser, + }) => { + const team = await testDatabase.seeder.createUserTeam( + "teamToUpdate", + registeredApiUser.user.id, ); + const response = await adminApiUser.context.delete(`teams/${team.id}`); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); }); - }, -); + + test("Team Member cannot delete team", async ({ orgTeamApiUser }) => { + const response = await orgTeamApiUser.context.delete( + `teams/${orgTeamApiUser.user.teams[0].id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + + test("Non-team member cannot delete team", async ({ + orgTeamApiUser, + registeredApiUser, + }) => { + const response = await registeredApiUser.context.delete( + `teams/${orgTeamApiUser.user.teams[0].id}`, + ); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(403); + }); + }); +}); diff --git a/e2e/tests/api/user.spec.ts b/e2e/tests/api/user.spec.ts index a51834e8..69ed4935 100644 --- a/e2e/tests/api/user.spec.ts +++ b/e2e/tests/api/user.spec.ts @@ -1,59 +1,57 @@ -import { expect, test } from "@playwright/test"; -import { adminAPIUser } from "../../fixtures/db/adminapi-user"; -import { apiUser } from "../../fixtures/db/api-user"; -import { baseUrl } from "../../playwright.config"; - -const baseURL = `${baseUrl}/api/`; -const userProfileEndpoint = `auth/user`; - -// Request context is reused by all tests in the file. -let apiContext; -let adminApiContext; -let adminUser; -let user; - -test.beforeAll(async ({ playwright }) => { - apiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": apiUser.apikey, - }, - }); - adminApiContext = await playwright.request.newContext({ - baseURL, - extraHTTPHeaders: { - "X-API-Key": adminAPIUser.apikey, - }, - }); - const au = await adminApiContext.get(userProfileEndpoint); - const auj = await au.json(); - adminUser = auj.data; - const u = await apiContext.get(userProfileEndpoint); - const uj = await u.json(); - user = uj.data; -}); - -test.afterAll(async ({}) => { - // Dispose all responses. - await apiContext.dispose(); -}); +import { expect, test } from "@fixtures/test-setup"; test.describe( - "registered user", + "User Profile API", { tag: ["@api", "@user", "@registered"] }, () => { - test(`GET ${userProfileEndpoint} should return session user profile`, async () => { - const u = await apiContext.get(userProfileEndpoint); - expect(u.ok()).toBeTruthy(); + const userProfileEndpoint = `auth/user`; - const pu = await u.json(); - expect(pu.data).toMatchObject({ - name: "E2E API User", + test("GET /auth/user returns session user profile for registered user", async ({ + request, + registeredApiUser, + }) => { + const response = await registeredApiUser.context.get(userProfileEndpoint); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + + const userProfile = await response.json(); + expect(userProfile.data).toMatchObject({ + id: registeredApiUser.user.id, + name: "E2EAPIUser", email: "e2eapi@thunderdome.dev", rank: "REGISTERED", verified: true, disabled: false, }); }); + + test("GET /auth/user returns session user profile for admin user", async ({ + request, + adminApiUser, + }) => { + const response = await adminApiUser.context.get(userProfileEndpoint); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + + const userProfile = await response.json(); + expect(userProfile.data).toMatchObject({ + id: adminApiUser.user.id, + rank: "ADMIN", + verified: true, + disabled: false, + }); + }); + + test("GET /auth/user returns 401 for unauthenticated request", async ({ + request, + }) => { + const response = await request.get(`api/${userProfileEndpoint}`, { + headers: { + "X-API-Key": "invalid_api_key", + }, + }); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(401); + }); }, ); diff --git a/e2e/tests/checkin.spec.ts b/e2e/tests/checkin.spec.ts index 71a988cd..7ed8b956 100644 --- a/e2e/tests/checkin.spec.ts +++ b/e2e/tests/checkin.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "../fixtures/user-sessions"; -import { TeamCheckinPage } from "../fixtures/checkin-page"; +import { TeamCheckinPage } from "../fixtures/pages/checkin-page"; test.describe("Team Checkin page", { tag: "@checkin" }, () => { test.describe("Unauthenticated user", { tag: "@unauthenticated" }, () => { diff --git a/e2e/tests/department.spec.ts b/e2e/tests/department.spec.ts index 2f1aaed8..3f761498 100644 --- a/e2e/tests/department.spec.ts +++ b/e2e/tests/department.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "../fixtures/user-sessions"; -import { DepartmentPage } from "../fixtures/department-page"; +import { DepartmentPage } from "../fixtures/pages/department-page"; test.describe("Department page", { tag: "@department" }, () => { test.describe("Unauthenticated user", { tag: "@unauthenticated" }, () => { diff --git a/e2e/tests/login.spec.ts b/e2e/tests/login.spec.ts index 6b125316..95a45869 100644 --- a/e2e/tests/login.spec.ts +++ b/e2e/tests/login.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "../fixtures/user-sessions"; -import { LoginPage } from "../fixtures/login-page"; +import { LoginPage } from "../fixtures/pages/login-page"; import { registeredUser } from "../fixtures/db/registered-user"; test.describe("The Login Page", { tag: "@login" }, () => { @@ -8,7 +8,7 @@ test.describe("The Login Page", { tag: "@login" }, () => { }) => { const loginPage = new LoginPage(page); await loginPage.goto(); - await loginPage.login(registeredUser.email, registeredUser.password); + await loginPage.login(registeredUser.email, "kentRules!"); await expect(loginPage.page.locator("h1")).toHaveText("My Games"); // UI should reflect this user being logged in diff --git a/e2e/tests/organization.spec.ts b/e2e/tests/organization.spec.ts index 44631471..1079e47a 100644 --- a/e2e/tests/organization.spec.ts +++ b/e2e/tests/organization.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "../fixtures/user-sessions"; -import { OrganizationPage } from "../fixtures/organization-page"; +import { OrganizationPage } from "../fixtures/pages/organization-page"; test.describe("Organization Page", { tag: "@organization" }, () => { test( diff --git a/e2e/tests/poker-game.spec.ts b/e2e/tests/poker-game.spec.ts index d41933cc..73596f8f 100644 --- a/e2e/tests/poker-game.spec.ts +++ b/e2e/tests/poker-game.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "../fixtures/user-sessions"; -import { PokerGamePage } from "../fixtures/poker-game-page"; +import { PokerGamePage } from "../fixtures/pages/poker-game-page"; const allowedPointValues = ["0", "1", "2", "3", "5", "8", "13", "?"]; const pointAverageRounding = "ceil"; diff --git a/e2e/tests/poker-games.spec.ts b/e2e/tests/poker-games.spec.ts index 285e6d90..da4bc955 100644 --- a/e2e/tests/poker-games.spec.ts +++ b/e2e/tests/poker-games.spec.ts @@ -1,6 +1,6 @@ import { test } from "../fixtures/user-sessions"; import { expect } from "@playwright/test"; -import { PokerGamesPage } from "../fixtures/poker-games-page"; +import { PokerGamesPage } from "../fixtures/pages/poker-games-page"; const pageTitle = "My Games"; diff --git a/e2e/tests/profile.spec.ts b/e2e/tests/profile.spec.ts index 34720769..34422180 100644 --- a/e2e/tests/profile.spec.ts +++ b/e2e/tests/profile.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "../fixtures/user-sessions"; -import { ProfilePage } from "../fixtures/profile-page"; +import { ProfilePage } from "../fixtures/pages/profile-page"; test.describe("User Profile page", { tag: ["@user"] }, () => { test( diff --git a/e2e/tests/register.spec.ts b/e2e/tests/register.spec.ts index f650f918..75764dca 100644 --- a/e2e/tests/register.spec.ts +++ b/e2e/tests/register.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { RegisterPage } from "../fixtures/register-page"; +import { RegisterPage } from "../fixtures/pages/register-page"; const registerPageTitle = "Register"; const battlesPageTitle = "My Games"; diff --git a/e2e/tests/retro.spec.ts b/e2e/tests/retro.spec.ts index 4bb166eb..8e9bf881 100644 --- a/e2e/tests/retro.spec.ts +++ b/e2e/tests/retro.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "../fixtures/user-sessions"; -import { RetroPage } from "../fixtures/retro-page"; +import { RetroPage } from "../fixtures/pages/retro-page"; test.describe("Retro page", { tag: ["@retro"] }, () => { let retro = { id: "", name: "e2e retro page tests" }; diff --git a/e2e/tests/retros.spec.ts b/e2e/tests/retros.spec.ts index 5d74b9c8..be1632fe 100644 --- a/e2e/tests/retros.spec.ts +++ b/e2e/tests/retros.spec.ts @@ -1,6 +1,6 @@ import { test } from "../fixtures/user-sessions"; import { expect } from "@playwright/test"; -import { RetrosPage } from "../fixtures/retros-page"; +import { RetrosPage } from "../fixtures/pages/retros-page"; const pageTitle = "My Retros"; diff --git a/e2e/tests/storyboard.spec.ts b/e2e/tests/storyboard.spec.ts index 3abbec94..8f480a71 100644 --- a/e2e/tests/storyboard.spec.ts +++ b/e2e/tests/storyboard.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "../fixtures/user-sessions"; -import { StoryboardPage } from "../fixtures/storyboard-page"; +import { StoryboardPage } from "../fixtures/pages/storyboard-page"; test.describe("Storyboard page", { tag: ["@storyboard"] }, () => { let storyboard = { id: "", name: "e2e storyboard page tests" }; diff --git a/e2e/tests/storyboards.spec.ts b/e2e/tests/storyboards.spec.ts index 90e57cab..8b0a7bfa 100644 --- a/e2e/tests/storyboards.spec.ts +++ b/e2e/tests/storyboards.spec.ts @@ -1,6 +1,6 @@ import { test } from "../fixtures/user-sessions"; import { expect } from "@playwright/test"; -import { StoryboardsPage } from "../fixtures/storyboards-page"; +import { StoryboardsPage } from "../fixtures/pages/storyboards-page"; const pageTitle = "My Storyboards"; diff --git a/e2e/tests/team.spec.ts b/e2e/tests/team.spec.ts index 6666ef1b..6087579f 100644 --- a/e2e/tests/team.spec.ts +++ b/e2e/tests/team.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "../fixtures/user-sessions"; -import { TeamPage } from "../fixtures/team-page"; +import { TeamPage } from "../fixtures/pages/team-page"; test.describe("Team page", { tag: ["@team"] }, () => { test( diff --git a/e2e/tests/teams.spec.ts b/e2e/tests/teams.spec.ts index 8b9157b4..06cf91ca 100644 --- a/e2e/tests/teams.spec.ts +++ b/e2e/tests/teams.spec.ts @@ -1,6 +1,6 @@ import { test } from "../fixtures/user-sessions"; import { expect } from "@playwright/test"; -import { TeamsPage } from "../fixtures/teams-page"; +import { TeamsPage } from "../fixtures/pages/teams-page"; test.describe("Teams page", { tag: ["@team"] }, () => { test.describe("Unauthenticated user", { tag: ["@unauthenticated"] }, () => { diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 00000000..7fc7327b --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@fixtures/*": [ + "fixtures/*" + ] + }, + "types": [ + "node", + "@playwright/test" + ], + "esModuleInterop": true, + "strict": true, + "include": [ + "tests/**/*.ts", + "playwright.config.ts" + ] + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod index bcdf720b..c0115e7f 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,11 @@ require ( google.golang.org/grpc v1.59.0 ) -require github.com/swaggo/http-swagger/v2 v2.0.2 +require ( + github.com/stretchr/testify v1.9.0 + github.com/swaggo/http-swagger/v2 v2.0.2 + github.com/unrolled/secure v1.15.0 +) require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect @@ -51,6 +55,7 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect @@ -85,6 +90,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -94,12 +100,12 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tidwall/gjson v1.16.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - github.com/unrolled/secure v1.15.0 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.1.21 // indirect github.com/vanng822/css v1.0.1 // indirect github.com/vanng822/go-premailer v1.20.1 // indirect diff --git a/go.sum b/go.sum index 495c3a4b..a54047e5 100644 --- a/go.sum +++ b/go.sum @@ -350,8 +350,9 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cma github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/internal/cookie/auth.go b/internal/cookie/auth.go index e18f3b91..a7da8075 100644 --- a/internal/cookie/auth.go +++ b/internal/cookie/auth.go @@ -7,13 +7,13 @@ import ( ) // CreateAuthStateCookie creates the oauth2 state validation cookie -func (s *Cookie) CreateAuthStateCookie(w http.ResponseWriter, state string) error { +func (s *CookieService) CreateAuthStateCookie(w http.ResponseWriter, state string) error { return s.CreateCookie(w, s.config.AuthStateCookieName, state, int(time.Minute.Seconds()*10)) } // ValidateAuthStateCookie retrieves the authstate cookie and validates its value against the state from // auth provider and deletes the authstate cookie -func (s *Cookie) ValidateAuthStateCookie(w http.ResponseWriter, r *http.Request, state string) error { +func (s *CookieService) ValidateAuthStateCookie(w http.ResponseWriter, r *http.Request, state string) error { cookieVal, err := s.GetCookie(w, r, s.config.AuthStateCookieName) if err != nil { return err @@ -29,6 +29,6 @@ func (s *Cookie) ValidateAuthStateCookie(w http.ResponseWriter, r *http.Request, } // DeleteAuthStateCookie deletes the oauth2 state validation cookie -func (s *Cookie) DeleteAuthStateCookie(w http.ResponseWriter) error { +func (s *CookieService) DeleteAuthStateCookie(w http.ResponseWriter) error { return s.CreateCookie(w, s.config.AuthStateCookieName, "", -1) } diff --git a/internal/cookie/cookie.go b/internal/cookie/cookie.go index daa90b99..08864643 100644 --- a/internal/cookie/cookie.go +++ b/internal/cookie/cookie.go @@ -7,8 +7,8 @@ import ( "github.com/gorilla/securecookie" ) -func New(config Config) *Cookie { - c := Cookie{ +func New(config Config) *CookieService { + c := CookieService{ config: config, } @@ -18,7 +18,7 @@ func New(config Config) *Cookie { } // CreateCookie creates a secure cookie with given cookieName -func (s *Cookie) CreateCookie(w http.ResponseWriter, cookieName string, value string, maxAge int) error { +func (s *CookieService) CreateCookie(w http.ResponseWriter, cookieName string, value string, maxAge int) error { encoded, err := s.sc.Encode(cookieName, value) if err != nil { return err @@ -41,7 +41,7 @@ func (s *Cookie) CreateCookie(w http.ResponseWriter, cookieName string, value st } // GetCookie returns the value from the cookie or errors if failure getting it -func (s *Cookie) GetCookie(w http.ResponseWriter, r *http.Request, cookieName string) (string, error) { +func (s *CookieService) GetCookie(w http.ResponseWriter, r *http.Request, cookieName string) (string, error) { var value string if cookie, err := r.Cookie(cookieName); err == nil { @@ -57,7 +57,7 @@ func (s *Cookie) GetCookie(w http.ResponseWriter, r *http.Request, cookieName st } // DeleteCookie deletes the cookie -func (s *Cookie) DeleteCookie(w http.ResponseWriter, cookieName string) { +func (s *CookieService) DeleteCookie(w http.ResponseWriter, cookieName string) { cookie := &http.Cookie{ Name: cookieName, Value: "", diff --git a/internal/cookie/types.go b/internal/cookie/types.go index 013466ed..98d8c615 100644 --- a/internal/cookie/types.go +++ b/internal/cookie/types.go @@ -2,7 +2,7 @@ package cookie import "github.com/gorilla/securecookie" -type Cookie struct { +type CookieService struct { config Config sc *securecookie.SecureCookie } diff --git a/internal/cookie/user.go b/internal/cookie/user.go index 74ec6358..8fe48836 100644 --- a/internal/cookie/user.go +++ b/internal/cookie/user.go @@ -35,18 +35,18 @@ func createJsonCookieValue(value any) (string, error) { return s, nil } -// CreateUserCookie creates the users Cookie -func (s *Cookie) CreateUserCookie(w http.ResponseWriter, UserID string) error { +// CreateUserCookie creates the users CookieService +func (s *CookieService) CreateUserCookie(w http.ResponseWriter, UserID string) error { return s.CreateCookie(w, s.config.SecureCookieName, UserID, int(time.Hour.Seconds()*24*365)) } -// CreateSessionCookie creates the user's session Cookie -func (s *Cookie) CreateSessionCookie(w http.ResponseWriter, SessionID string) error { +// CreateSessionCookie creates the user's session CookieService +func (s *CookieService) CreateSessionCookie(w http.ResponseWriter, SessionID string) error { return s.CreateCookie(w, s.config.SessionCookieName, SessionID, int(time.Hour.Seconds()*24*30)) } // CreateUserUICookie creates the user's frontend UI cookie -func (s *Cookie) CreateUserUICookie(w http.ResponseWriter, userUiCookie thunderdome.UserUICookie) error { +func (s *CookieService) CreateUserUICookie(w http.ResponseWriter, userUiCookie thunderdome.UserUICookie) error { encodedValue, err := createJsonCookieValue(userUiCookie) if err != nil { return fmt.Errorf("error creating encoded json for cookie: %w", err) @@ -66,8 +66,8 @@ func (s *Cookie) CreateUserUICookie(w http.ResponseWriter, userUiCookie thunderd } // ClearUserCookies wipes the frontend and backend cookies -// used in the event of bad Cookie reads -func (s *Cookie) ClearUserCookies(w http.ResponseWriter) { +// used in the event of bad CookieService reads +func (s *CookieService) ClearUserCookies(w http.ResponseWriter) { s.DeleteCookie(w, s.config.SecureCookieName) s.DeleteCookie(w, s.config.SessionCookieName) @@ -81,11 +81,11 @@ func (s *Cookie) ClearUserCookies(w http.ResponseWriter) { } // ValidateUserCookie returns the UserID from secure cookies or errors if failures getting it -func (s *Cookie) ValidateUserCookie(w http.ResponseWriter, r *http.Request) (string, error) { +func (s *CookieService) ValidateUserCookie(w http.ResponseWriter, r *http.Request) (string, error) { return s.GetCookie(w, r, s.config.SecureCookieName) } // ValidateSessionCookie returns the SessionID from secure cookies or errors if failures getting it -func (s *Cookie) ValidateSessionCookie(w http.ResponseWriter, r *http.Request) (string, error) { +func (s *CookieService) ValidateSessionCookie(w http.ResponseWriter, r *http.Request) (string, error) { return s.GetCookie(w, r, s.config.SessionCookieName) } diff --git a/internal/db/team/organization.go b/internal/db/team/organization.go index 9bd0e366..934838bb 100644 --- a/internal/db/team/organization.go +++ b/internal/db/team/organization.go @@ -38,8 +38,10 @@ func (d *OrganizationService) OrganizationGet(ctx context.Context, OrgID string) &org.UpdatedDate, &org.Subscribed, ) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("error getting organization: %v", err) + } else if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("ORGANIZATION_NOT_FOUND") } return org, nil @@ -58,8 +60,10 @@ func (d *OrganizationService) OrganizationUserRole(ctx context.Context, UserID s ).Scan( &role, ) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { return "", fmt.Errorf("error getting organization users role: %v", err) + } else if err != nil && errors.Is(err, sql.ErrNoRows) { + return "", fmt.Errorf("USER_ROLE_NOT_FOUND") } return role, nil diff --git a/internal/db/team/user.go b/internal/db/team/user.go index 8aa28721..8582c431 100644 --- a/internal/db/team/user.go +++ b/internal/db/team/user.go @@ -2,6 +2,8 @@ package team import ( "context" + "database/sql" + "errors" "fmt" "github.com/StevenWeathers/thunderdome-planning-poker/internal/db" @@ -29,6 +31,76 @@ func (d *Service) TeamUserRole(ctx context.Context, UserID string, TeamID string return teamRole, nil } +// TeamUserRoles gets a user's set of roles in relation to the team if any, and if application the department and organization +func (d *Service) TeamUserRoles(ctx context.Context, UserID string, TeamID string) (*thunderdome.UserTeamRoleInfo, error) { + tr := thunderdome.UserTeamRoleInfo{} + + err := d.DB.QueryRowContext(ctx, + `WITH team_info AS ( + SELECT + t.id AS team_id, + t.department_id, + COALESCE(t.organization_id, od.organization_id) AS organization_id + FROM + thunderdome.team t + LEFT JOIN + thunderdome.organization_department od ON t.department_id = od.id + WHERE + t.id = $2 +), +user_roles AS ( + SELECT + ti.*, + $1 AS user_id, + tu.role AS team_role, + du.role AS department_role, + ou.role AS organization_role + FROM + team_info ti + LEFT JOIN + thunderdome.team_user tu ON ti.team_id = tu.team_id AND tu.user_id = $1 + LEFT JOIN + thunderdome.department_user du ON ti.department_id = du.department_id AND du.user_id = $1 + LEFT JOIN + thunderdome.organization_user ou ON ti.organization_id = ou.organization_id AND ou.user_id = $1 +) +SELECT + user_id, + team_id, + team_role, + department_id, + department_role, + organization_id, + organization_role, + CASE + WHEN team_role IS NOT NULL THEN 'TEAM' + WHEN department_role IS NOT NULL THEN 'DEPARTMENT' + WHEN organization_role IS NOT NULL THEN 'ORGANIZATION' + ELSE 'NONE' + END AS association_level +FROM + user_roles;`, + UserID, + TeamID, + ).Scan( + &tr.UserID, + &tr.TeamID, + &tr.TeamRole, + &tr.DepartmentID, + &tr.DepartmentRole, + &tr.OrganizationID, + &tr.OrganizationRole, + &tr.AssociationLevel, + ) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("TEAM_NOT_FOUND") + } else if err != nil { + return nil, fmt.Errorf("error getting team users roles: %v", err) + } + + return &tr, nil +} + // TeamUserList gets a list of team users func (d *Service) TeamUserList(ctx context.Context, TeamID string, Limit int, Offset int) ([]*thunderdome.TeamUser, int, error) { var users = make([]*thunderdome.TeamUser, 0) diff --git a/internal/http/auth.go b/internal/http/auth.go index 5ed81c53..076e094c 100644 --- a/internal/http/auth.go +++ b/internal/http/auth.go @@ -273,7 +273,7 @@ func (s *Service) handleMFALogin() http.HandlerFunc { } } -// handleLogout clears the user Cookie(s) ending session +// handleLogout clears the user CookieManager(s) ending session // @Summary Logout // @Description Logs the user out by deleting session cookies // @Tags auth diff --git a/internal/http/checkin/checkin.go b/internal/http/checkin/checkin.go index c0398530..b5ab1312 100644 --- a/internal/http/checkin/checkin.go +++ b/internal/http/checkin/checkin.go @@ -38,6 +38,26 @@ func (c *Config) PongWait() time.Duration { return time.Duration(c.PongWaitSec) * time.Second } +type CheckinDataSvc interface { + CheckinList(ctx context.Context, TeamId string, Date string, TimeZone string) ([]*thunderdome.TeamCheckin, error) + CheckinCreate(ctx context.Context, TeamId string, UserId string, Yesterday string, Today string, Blockers string, Discuss string, GoalsMet bool) error + CheckinUpdate(ctx context.Context, CheckinId string, Yesterday string, Today string, Blockers string, Discuss string, GoalsMet bool) error + CheckinDelete(ctx context.Context, CheckinId string) error + CheckinComment(ctx context.Context, TeamId string, CheckinId string, UserId string, Comment string) error + CheckinCommentEdit(ctx context.Context, TeamId string, UserId string, CommentId string, Comment string) error + CheckinCommentDelete(ctx context.Context, CommentId string) error + CheckinLastByUser(ctx context.Context, TeamId string, UserId string) (*thunderdome.TeamCheckin, error) +} + +type AuthDataSvc interface { + GetSessionUser(ctx context.Context, SessionId string) (*thunderdome.User, error) +} + +type TeamDataSvc interface { + TeamUserRole(ctx context.Context, UserID string, TeamID string) (string, error) + TeamGet(ctx context.Context, TeamID string) (*thunderdome.Team, error) +} + // Service provides retro service type Service struct { config Config @@ -46,9 +66,9 @@ type Service struct { validateUserCookie func(w http.ResponseWriter, r *http.Request) (string, error) eventHandlers map[string]func(context.Context, string, string, string) ([]byte, error, bool) UserService thunderdome.UserDataSvc - AuthService thunderdome.AuthDataSvc - CheckinService thunderdome.CheckinDataSvc - TeamService thunderdome.TeamDataSvc + AuthService AuthDataSvc + CheckinService CheckinDataSvc + TeamService TeamDataSvc } // New returns a new retro with websocket hub/client and event handlers @@ -57,8 +77,8 @@ func New( logger *otelzap.Logger, validateSessionCookie func(w http.ResponseWriter, r *http.Request) (string, error), validateUserCookie func(w http.ResponseWriter, r *http.Request) (string, error), - userService thunderdome.UserDataSvc, authService thunderdome.AuthDataSvc, - checkinService thunderdome.CheckinDataSvc, teamService thunderdome.TeamDataSvc, + userService thunderdome.UserDataSvc, authService AuthDataSvc, + checkinService CheckinDataSvc, teamService TeamDataSvc, ) *Service { c := &Service{ config: config, diff --git a/internal/http/department.go b/internal/http/department.go index b43456a1..3b2a0eef 100644 --- a/internal/http/department.go +++ b/internal/http/department.go @@ -648,9 +648,20 @@ func (s *Service) handleDepartmentTeamByUser() http.HandlerFunc { } ctx := r.Context() SessionUserID := ctx.Value(contextKeyUserID).(string) - OrgRole := ctx.Value(contextKeyOrgRole).(string) - DepartmentRole := ctx.Value(contextKeyDepartmentRole).(string) - TeamRole := ctx.Value(contextKeyTeamRole).(string) + TeamUserRoles := ctx.Value(contextKeyUserTeamRoles).(*thunderdome.UserTeamRoleInfo) + var emptyRole = "" + OrgRole := TeamUserRoles.OrganizationRole + if OrgRole == nil { + OrgRole = &emptyRole + } + DepartmentRole := TeamUserRoles.DepartmentRole + if DepartmentRole == nil { + DepartmentRole = &emptyRole + } + TeamRole := TeamUserRoles.TeamRole + if TeamRole == nil { + TeamRole = &emptyRole + } vars := mux.Vars(r) OrgID := vars["orgId"] oidErr := validate.Var(OrgID, "required,uuid") @@ -705,9 +716,9 @@ func (s *Service) handleDepartmentTeamByUser() http.HandlerFunc { Organization: Organization, Department: Department, Team: Team, - OrganizationRole: OrgRole, - DepartmentRole: DepartmentRole, - TeamRole: TeamRole, + OrganizationRole: *OrgRole, + DepartmentRole: *DepartmentRole, + TeamRole: *TeamRole, } s.Success(w, r, http.StatusOK, result, nil) diff --git a/internal/http/http.go b/internal/http/http.go index 9d7a2560..5279367f 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -218,47 +218,47 @@ func New(apiService Service, FSS fs.FS, HFS http.FileSystem) *Service { orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/users/{userId}", a.userOnly(a.departmentAdminOnly(a.handleDepartmentRemoveUser()))).Methods("DELETE") orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams", a.userOnly(a.departmentUserOnly(a.handleGetDepartmentTeams()))).Methods("GET") orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams", a.userOnly(a.departmentAdminOnly(a.handleCreateDepartmentTeam()))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}", a.userOnly(a.departmentTeamUserOnly(a.handleDepartmentTeamByUser()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}", a.userOnly(a.teamUserOnly(a.handleDepartmentTeamByUser()))).Methods("GET") orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}", a.userOnly(a.departmentAdminOnly(a.handleTeamUpdate()))).Methods("PUT") orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}", a.userOnly(a.departmentAdminOnly(a.handleDeleteTeam()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/invites", a.userOnly(a.departmentTeamUserOnly(a.handleGetTeamUserInvites()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/invites", a.userOnly(a.departmentTeamUserOnly(a.handleTeamInviteUser()))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/invites/{inviteId}", a.userOnly(a.departmentTeamAdminOnly(a.handleDeleteTeamUserInvite()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users", a.userOnly(a.departmentTeamUserOnly(a.handleGetTeamUsers()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users", a.userOnly(a.departmentTeamAdminOnly(a.handleDepartmentTeamAddUser()))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}", a.userOnly(a.departmentTeamAdminOnly(a.handleTeamUpdateUser()))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}", a.userOnly(a.departmentTeamAdminOnly(a.handleTeamRemoveUser()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins", a.userOnly(a.departmentTeamUserOnly(a.handleCheckinsGet()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins", a.userOnly(a.departmentTeamUserOnly(a.handleCheckinCreate(checkinSvc)))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/users/{userId}/last", a.userOnly(a.subscribedUserOnly(a.departmentTeamUserOnly(a.handleCheckinLastByUser())))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}", a.userOnly(a.departmentTeamUserOnly(a.handleCheckinUpdate(checkinSvc)))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}", a.userOnly(a.departmentTeamUserOnly(a.handleCheckinDelete(checkinSvc)))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}/comments", a.userOnly(a.departmentTeamUserOnly(a.handleCheckinComment(checkinSvc)))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}/comments/{commentId}", a.userOnly(a.departmentTeamUserOnly(a.handleCheckinCommentEdit(checkinSvc)))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}/comments/{commentId}", a.userOnly(a.departmentTeamUserOnly(a.handleCheckinCommentDelete(checkinSvc)))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/metrics", a.userOnly(a.departmentTeamUserOnly(a.handleTeamMetrics()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/invites", a.userOnly(a.teamUserOnly(a.handleGetTeamUserInvites()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/invites", a.userOnly(a.teamUserOnly(a.handleTeamInviteUser()))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/invites/{inviteId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleDeleteTeamUserInvite())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users", a.userOnly(a.teamUserOnly(a.handleGetTeamUsers()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleDepartmentTeamAddUser())))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamUpdateUser())))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveUser())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins", a.userOnly(a.teamUserOnly(a.handleCheckinsGet()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins", a.userOnly(a.teamUserOnly(a.handleCheckinCreate(checkinSvc)))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/users/{userId}/last", a.userOnly(a.subscribedUserOnly(a.teamUserOnly(a.handleCheckinLastByUser())))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}", a.userOnly(a.teamUserOnly(a.handleCheckinUpdate(checkinSvc)))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}", a.userOnly(a.teamUserOnly(a.handleCheckinDelete(checkinSvc)))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}/comments", a.userOnly(a.teamUserOnly(a.handleCheckinComment(checkinSvc)))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}/comments/{commentId}", a.userOnly(a.teamUserOnly(a.handleCheckinCommentEdit(checkinSvc)))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/checkins/{checkinId}/comments/{commentId}", a.userOnly(a.teamUserOnly(a.handleCheckinCommentDelete(checkinSvc)))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/metrics", a.userOnly(a.teamUserOnly(a.handleTeamMetrics()))).Methods("GET") // org teams orgRouter.HandleFunc("/{orgId}/teams", a.userOnly(a.orgUserOnly(a.handleGetOrganizationTeams()))).Methods("GET") orgRouter.HandleFunc("/{orgId}/teams", a.userOnly(a.orgAdminOnly(a.handleCreateOrganizationTeam()))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}", a.userOnly(a.orgTeamOnly(a.handleGetOrganizationTeamByUser()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}", a.userOnly(a.teamUserOnly(a.handleGetOrganizationTeamByUser()))).Methods("GET") orgRouter.HandleFunc("/{orgId}/teams/{teamId}", a.userOnly(a.orgAdminOnly(a.handleTeamUpdate()))).Methods("PUT") orgRouter.HandleFunc("/{orgId}/teams/{teamId}", a.userOnly(a.orgAdminOnly(a.handleDeleteTeam()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/invites", a.userOnly(a.orgTeamOnly(a.handleGetTeamUserInvites()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/invites", a.userOnly(a.orgTeamOnly(a.handleTeamInviteUser()))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/invites/{inviteId}", a.userOnly(a.orgTeamAdminOnly(a.handleDeleteTeamUserInvite()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users", a.userOnly(a.orgTeamOnly(a.handleGetTeamUsers()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users", a.userOnly(a.orgTeamAdminOnly(a.handleOrganizationTeamAddUser()))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}", a.userOnly(a.orgTeamAdminOnly(a.handleTeamUpdateUser()))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}", a.userOnly(a.orgTeamAdminOnly(a.handleTeamRemoveUser()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins", a.userOnly(a.orgTeamOnly(a.handleCheckinsGet()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins", a.userOnly(a.orgTeamOnly(a.handleCheckinCreate(checkinSvc)))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/users/{userId}/last", a.userOnly(a.subscribedUserOnly(a.orgTeamOnly(a.handleCheckinLastByUser())))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}", a.userOnly(a.orgTeamOnly(a.handleCheckinUpdate(checkinSvc)))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}", a.userOnly(a.orgTeamOnly(a.handleCheckinDelete(checkinSvc)))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}/comments", a.userOnly(a.orgTeamOnly(a.handleCheckinComment(checkinSvc)))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}/comments/{commentId}", a.userOnly(a.orgTeamOnly(a.handleCheckinCommentEdit(checkinSvc)))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}/comments/{commentId}", a.userOnly(a.orgTeamOnly(a.handleCheckinCommentDelete(checkinSvc)))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/metrics", a.userOnly(a.orgTeamOnly(a.handleTeamMetrics()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/invites", a.userOnly(a.teamUserOnly(a.handleGetTeamUserInvites()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/invites", a.userOnly(a.teamUserOnly(a.handleTeamInviteUser()))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/invites/{inviteId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleDeleteTeamUserInvite())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users", a.userOnly(a.teamUserOnly(a.handleGetTeamUsers()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleOrganizationTeamAddUser())))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamUpdateUser())))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveUser())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins", a.userOnly(a.teamUserOnly(a.handleCheckinsGet()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins", a.userOnly(a.teamUserOnly(a.handleCheckinCreate(checkinSvc)))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/users/{userId}/last", a.userOnly(a.subscribedUserOnly(a.teamUserOnly(a.handleCheckinLastByUser())))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}", a.userOnly(a.teamUserOnly(a.handleCheckinUpdate(checkinSvc)))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}", a.userOnly(a.teamUserOnly(a.handleCheckinDelete(checkinSvc)))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}/comments", a.userOnly(a.teamUserOnly(a.handleCheckinComment(checkinSvc)))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}/comments/{commentId}", a.userOnly(a.teamUserOnly(a.handleCheckinCommentEdit(checkinSvc)))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/checkins/{checkinId}/comments/{commentId}", a.userOnly(a.teamUserOnly(a.handleCheckinCommentDelete(checkinSvc)))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/metrics", a.userOnly(a.teamUserOnly(a.handleTeamMetrics()))).Methods("GET") // org users orgRouter.HandleFunc("/{orgId}/users", a.userOnly(a.orgUserOnly(a.handleGetOrganizationUsers()))).Methods("GET") orgRouter.HandleFunc("/{orgId}/users/{userId}", a.userOnly(a.orgAdminOnly(a.handleOrganizationUpdateUser()))).Methods("PUT") @@ -268,14 +268,14 @@ func New(apiService Service, FSS fs.FS, HFS http.FileSystem) *Service { orgRouter.HandleFunc("/{orgId}/invites/{inviteId}", a.userOnly(a.orgAdminOnly(a.handleDeleteOrganizationUserInvite()))).Methods("DELETE") // teams(s) teamRouter.HandleFunc("/{teamId}", a.userOnly(a.teamUserOnly(a.handleGetTeamByUser()))).Methods("GET") - teamRouter.HandleFunc("/{teamId}", a.userOnly(a.teamAdminOnly(a.handleTeamUpdate()))).Methods("PUT") - teamRouter.HandleFunc("/{teamId}", a.userOnly(a.teamAdminOnly(a.handleDeleteTeam()))).Methods("DELETE") + teamRouter.HandleFunc("/{teamId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamUpdate())))).Methods("PUT") + teamRouter.HandleFunc("/{teamId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleDeleteTeam())))).Methods("DELETE") teamRouter.HandleFunc("/{teamId}/invites", a.userOnly(a.teamUserOnly(a.handleGetTeamUserInvites()))).Methods("GET") - teamRouter.HandleFunc("/{teamId}/invites", a.userOnly(a.teamAdminOnly(a.handleTeamInviteUser()))).Methods("POST") - teamRouter.HandleFunc("/{teamId}/invites/{inviteId}", a.userOnly(a.teamAdminOnly(a.handleDeleteTeamUserInvite()))).Methods("DELETE") + teamRouter.HandleFunc("/{teamId}/invites", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamInviteUser())))).Methods("POST") + teamRouter.HandleFunc("/{teamId}/invites/{inviteId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleDeleteTeamUserInvite())))).Methods("DELETE") teamRouter.HandleFunc("/{teamId}/users", a.userOnly(a.teamUserOnly(a.handleGetTeamUsers()))).Methods("GET") - teamRouter.HandleFunc("/{teamId}/users/{userId}", a.userOnly(a.teamAdminOnly(a.handleTeamUpdateUser()))).Methods("PUT") - teamRouter.HandleFunc("/{teamId}/users/{userId}", a.userOnly(a.teamAdminOnly(a.handleTeamRemoveUser()))).Methods("DELETE") + teamRouter.HandleFunc("/{teamId}/users/{userId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamUpdateUser())))).Methods("PUT") + teamRouter.HandleFunc("/{teamId}/users/{userId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveUser())))).Methods("DELETE") teamRouter.HandleFunc("/{teamId}/checkin", checkinSvc.ServeWs()) teamRouter.HandleFunc("/{teamId}/checkins", a.userOnly(a.teamUserOnly(a.handleCheckinsGet()))).Methods("GET") teamRouter.HandleFunc("/{teamId}/checkins", a.userOnly(a.teamUserOnly(a.handleCheckinCreate(checkinSvc)))).Methods("POST") @@ -310,14 +310,14 @@ func New(apiService Service, FSS fs.FS, HFS http.FileSystem) *Service { if a.Config.FeaturePoker { userRouter.HandleFunc("/{userId}/battles", a.userOnly(a.entityUserOnly(a.handlePokerCreate()))).Methods("POST") userRouter.HandleFunc("/{userId}/battles", a.userOnly(a.entityUserOnly(a.handleGetUserGames()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/battles", a.userOnly(a.departmentTeamUserOnly(a.handleGetTeamBattles()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/battles/{battleId}", a.userOnly(a.departmentTeamAdminOnly(a.handleTeamRemoveBattle()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}/battles", a.userOnly(a.departmentTeamUserOnly(a.handlePokerCreate()))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/battles", a.userOnly(a.orgTeamOnly(a.handleGetTeamBattles()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/battles/{battleId}", a.userOnly(a.orgTeamAdminOnly(a.handleTeamRemoveBattle()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}/battles", a.userOnly(a.orgTeamOnly(a.entityUserOnly(a.handlePokerCreate())))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/battles", a.userOnly(a.teamUserOnly(a.handleGetTeamBattles()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/battles/{battleId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveBattle())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}/battles", a.userOnly(a.teamUserOnly(a.handlePokerCreate()))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/battles", a.userOnly(a.teamUserOnly(a.handleGetTeamBattles()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/battles/{battleId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveBattle())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}/battles", a.userOnly(a.teamUserOnly(a.entityUserOnly(a.handlePokerCreate())))).Methods("POST") teamRouter.HandleFunc("/{teamId}/battles", a.userOnly(a.teamUserOnly(a.handleGetTeamBattles()))).Methods("GET") - teamRouter.HandleFunc("/{teamId}/battles/{battleId}", a.userOnly(a.teamAdminOnly(a.handleTeamRemoveBattle()))).Methods("DELETE") + teamRouter.HandleFunc("/{teamId}/battles/{battleId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveBattle())))).Methods("DELETE") teamRouter.HandleFunc("/{teamId}/users/{userId}/battles", a.userOnly(a.teamUserOnly(a.entityUserOnly(a.handlePokerCreate())))).Methods("POST") apiRouter.HandleFunc("/maintenance/clean-battles", a.userOnly(a.adminOnly(a.handleCleanBattles()))).Methods("DELETE") apiRouter.HandleFunc("/battles", a.userOnly(a.adminOnly(a.handleGetPokerGames()))).Methods("GET") @@ -338,19 +338,19 @@ func New(apiService Service, FSS fs.FS, HFS http.FileSystem) *Service { orgRouter.HandleFunc("/{orgId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.orgAdminOnly(a.handleOrganizationEstimationScaleUpdate())))).Methods("PUT") orgRouter.HandleFunc("/{orgId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.orgAdminOnly(a.handleOrganizationEstimationScaleDelete())))).Methods("DELETE") orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/estimation-scales", a.userOnly(a.subscribedOrgOnly(a.departmentUserOnly(a.handleGetTeamEstimationScales())))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/estimation-scales", a.userOnly(a.subscribedOrgOnly(a.departmentTeamAdminOnly(a.handleTeamEstimationScaleCreate())))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.departmentTeamAdminOnly(a.handleTeamEstimationScaleUpdate())))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.departmentTeamAdminOnly(a.handleTeamEstimationScaleDelete())))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/estimation-scales", a.userOnly(a.subscribedOrgOnly(a.orgTeamOnly(a.handleGetTeamEstimationScales())))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/estimation-scales", a.userOnly(a.subscribedOrgOnly(a.orgTeamAdminOnly(a.handleTeamEstimationScaleCreate())))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.orgTeamAdminOnly(a.handleTeamEstimationScaleUpdate())))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.orgTeamAdminOnly(a.handleTeamEstimationScaleDelete())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/estimation-scales", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamEstimationScaleCreate()))))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamEstimationScaleUpdate()))))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamEstimationScaleDelete()))))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/estimation-scales", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.handleGetTeamEstimationScales())))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/estimation-scales", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamEstimationScaleCreate()))))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamEstimationScaleUpdate()))))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamEstimationScaleDelete()))))).Methods("DELETE") // Team-specific estimation scale routes teamRouter.HandleFunc("/{teamId}/estimation-scales", a.userOnly(a.subscribedTeamOnly(a.teamUserOnly(a.handleGetTeamEstimationScales())))).Methods("GET") - teamRouter.HandleFunc("/{teamId}/estimation-scales", a.userOnly(a.subscribedTeamOnly(a.teamAdminOnly(a.handleTeamEstimationScaleCreate())))).Methods("POST") - teamRouter.HandleFunc("/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedTeamOnly(a.teamAdminOnly(a.handleTeamEstimationScaleUpdate())))).Methods("PUT") - teamRouter.HandleFunc("/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedTeamOnly(a.teamAdminOnly(a.handleTeamEstimationScaleDelete())))).Methods("DELETE") + teamRouter.HandleFunc("/{teamId}/estimation-scales", a.userOnly(a.subscribedTeamOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamEstimationScaleCreate()))))).Methods("POST") + teamRouter.HandleFunc("/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedTeamOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamEstimationScaleUpdate()))))).Methods("PUT") + teamRouter.HandleFunc("/{teamId}/estimation-scales/{scaleId}", a.userOnly(a.subscribedTeamOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamEstimationScaleDelete()))))).Methods("DELETE") // Admin estimation scale routes adminRouter.HandleFunc("/estimation-scales", a.userOnly(a.adminOnly(a.handleGetEstimationScales()))).Methods("GET") @@ -362,16 +362,16 @@ func New(apiService Service, FSS fs.FS, HFS http.FileSystem) *Service { if a.Config.FeatureRetro { userRouter.HandleFunc("/{userId}/retros", a.userOnly(a.entityUserOnly(a.handleRetroCreate()))).Methods("POST") userRouter.HandleFunc("/{userId}/retros", a.userOnly(a.entityUserOnly(a.handleRetrosGetByUser()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retros", a.userOnly(a.departmentTeamUserOnly(a.handleGetTeamRetros()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retros/{retroId}", a.userOnly(a.departmentTeamAdminOnly(a.handleTeamRemoveRetro()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retro-actions", a.userOnly(a.departmentTeamUserOnly(a.handleGetTeamRetroActions()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}/retros", a.userOnly(a.departmentTeamUserOnly(a.handleRetroCreate()))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retros", a.userOnly(a.orgTeamOnly(a.handleGetTeamRetros()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-actions", a.userOnly(a.orgTeamOnly(a.handleGetTeamRetroActions()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retros/{retroId}", a.userOnly(a.orgTeamAdminOnly(a.handleTeamRemoveRetro()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}/retros", a.userOnly(a.orgTeamOnly(a.entityUserOnly(a.handleRetroCreate())))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retros", a.userOnly(a.teamUserOnly(a.handleGetTeamRetros()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retros/{retroId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveRetro())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retro-actions", a.userOnly(a.teamUserOnly(a.handleGetTeamRetroActions()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}/retros", a.userOnly(a.teamUserOnly(a.handleRetroCreate()))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retros", a.userOnly(a.teamUserOnly(a.handleGetTeamRetros()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-actions", a.userOnly(a.teamUserOnly(a.handleGetTeamRetroActions()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retros/{retroId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveRetro())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}/retros", a.userOnly(a.teamUserOnly(a.entityUserOnly(a.handleRetroCreate())))).Methods("POST") teamRouter.HandleFunc("/{teamId}/retros", a.userOnly(a.teamUserOnly(a.handleGetTeamRetros()))).Methods("GET") - teamRouter.HandleFunc("/{teamId}/retros/{retroId}", a.userOnly(a.teamAdminOnly(a.handleTeamRemoveRetro()))).Methods("DELETE") + teamRouter.HandleFunc("/{teamId}/retros/{retroId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveRetro())))).Methods("DELETE") teamRouter.HandleFunc("/{teamId}/retro-actions", a.userOnly(a.teamUserOnly(a.handleGetTeamRetroActions()))).Methods("GET") teamRouter.HandleFunc("/{teamId}/users/{userId}/retros", a.userOnly(a.teamUserOnly(a.entityUserOnly(a.handleRetroCreate())))).Methods("POST") apiRouter.HandleFunc("/maintenance/clean-retros", a.userOnly(a.adminOnly(a.handleCleanRetros()))).Methods("DELETE") @@ -394,18 +394,18 @@ func New(apiService Service, FSS fs.FS, HFS http.FileSystem) *Service { orgRouter.HandleFunc("/{orgId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.orgAdminOnly(a.handleOrganizationRetroTemplateUpdate())))).Methods("PUT") orgRouter.HandleFunc("/{orgId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.orgAdminOnly(a.handleOrganizationRetroTemplateDelete())))).Methods("DELETE") orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retro-templates", a.userOnly(a.subscribedOrgOnly(a.departmentUserOnly(a.handleGetTeamRetroTemplates())))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retro-templates", a.userOnly(a.subscribedOrgOnly(a.departmentTeamAdminOnly(a.handleTeamRetroTemplateCreate())))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.departmentTeamAdminOnly(a.handleTeamRetroTemplateUpdate())))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.departmentTeamAdminOnly(a.handleTeamRetroTemplateDelete())))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-templates", a.userOnly(a.subscribedOrgOnly(a.orgTeamOnly(a.handleGetTeamRetroTemplates())))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-templates", a.userOnly(a.subscribedOrgOnly(a.orgTeamAdminOnly(a.handleTeamRetroTemplateCreate())))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.orgTeamAdminOnly(a.handleTeamRetroTemplateUpdate())))).Methods("PUT") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.orgTeamAdminOnly(a.handleTeamRetroTemplateDelete())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retro-templates", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRetroTemplateCreate()))))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRetroTemplateUpdate()))))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRetroTemplateDelete()))))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-templates", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.handleGetTeamRetroTemplates())))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-templates", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRetroTemplateCreate()))))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRetroTemplateUpdate()))))).Methods("PUT") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedOrgOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRetroTemplateDelete()))))).Methods("DELETE") // Team templates teamRouter.HandleFunc("/{teamId}/retro-templates", a.userOnly(a.subscribedTeamOnly(a.teamUserOnly(a.handleGetTeamRetroTemplates())))).Methods("GET") - teamRouter.HandleFunc("/{teamId}/retro-templates", a.userOnly(a.subscribedTeamOnly(a.teamAdminOnly(a.handleTeamRetroTemplateCreate())))).Methods("POST") - teamRouter.HandleFunc("/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedTeamOnly(a.teamAdminOnly(a.handleTeamRetroTemplateUpdate())))).Methods("PUT") - teamRouter.HandleFunc("/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedTeamOnly(a.teamAdminOnly(a.handleTeamRetroTemplateDelete())))).Methods("DELETE") + teamRouter.HandleFunc("/{teamId}/retro-templates", a.userOnly(a.subscribedTeamOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRetroTemplateCreate()))))).Methods("POST") + teamRouter.HandleFunc("/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedTeamOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRetroTemplateUpdate()))))).Methods("PUT") + teamRouter.HandleFunc("/{teamId}/retro-templates/{templateId}", a.userOnly(a.subscribedTeamOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRetroTemplateDelete()))))).Methods("DELETE") // General template operations adminRouter.HandleFunc("/retro-templates", a.userOnly(a.adminOnly(a.handleGetRetroTemplates()))).Methods("GET") adminRouter.HandleFunc("/retro-templates/{templateId}", a.userOnly(a.adminOnly(a.handleGetRetroTemplateById()))).Methods("GET") @@ -419,14 +419,14 @@ func New(apiService Service, FSS fs.FS, HFS http.FileSystem) *Service { if a.Config.FeatureStoryboard { userRouter.HandleFunc("/{userId}/storyboards", a.userOnly(a.entityUserOnly(a.handleStoryboardCreate()))).Methods("POST") userRouter.HandleFunc("/{userId}/storyboards", a.userOnly(a.entityUserOnly(a.handleGetUserStoryboards()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/storyboards", a.userOnly(a.departmentTeamUserOnly(a.handleGetTeamStoryboards()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/storyboards/{storyboardId}", a.userOnly(a.departmentTeamAdminOnly(a.handleTeamRemoveStoryboard()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}/storyboards", a.userOnly(a.departmentTeamUserOnly(a.handleStoryboardCreate()))).Methods("POST") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/storyboards", a.userOnly(a.orgTeamOnly(a.handleGetTeamStoryboards()))).Methods("GET") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/storyboards/{storyboardId}", a.userOnly(a.orgTeamAdminOnly(a.handleTeamRemoveStoryboard()))).Methods("DELETE") - orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}/storyboards", a.userOnly(a.orgTeamOnly(a.entityUserOnly(a.handleStoryboardCreate())))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/storyboards", a.userOnly(a.teamUserOnly(a.handleGetTeamStoryboards()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/storyboards/{storyboardId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveStoryboard())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/departments/{departmentId}/teams/{teamId}/users/{userId}/storyboards", a.userOnly(a.teamUserOnly(a.handleStoryboardCreate()))).Methods("POST") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/storyboards", a.userOnly(a.teamUserOnly(a.handleGetTeamStoryboards()))).Methods("GET") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/storyboards/{storyboardId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveStoryboard())))).Methods("DELETE") + orgRouter.HandleFunc("/{orgId}/teams/{teamId}/users/{userId}/storyboards", a.userOnly(a.teamUserOnly(a.entityUserOnly(a.handleStoryboardCreate())))).Methods("POST") teamRouter.HandleFunc("/{teamId}/storyboards", a.userOnly(a.teamUserOnly(a.handleGetTeamStoryboards()))).Methods("GET") - teamRouter.HandleFunc("/{teamId}/storyboards/{storyboardId}", a.userOnly(a.teamAdminOnly(a.handleTeamRemoveStoryboard()))).Methods("DELETE") + teamRouter.HandleFunc("/{teamId}/storyboards/{storyboardId}", a.userOnly(a.teamUserOnly(a.teamAdminOnly(a.handleTeamRemoveStoryboard())))).Methods("DELETE") teamRouter.HandleFunc("/{teamId}/users/{userId}/storyboards", a.userOnly(a.teamUserOnly(a.entityUserOnly(a.handleStoryboardCreate())))).Methods("POST") apiRouter.HandleFunc("/maintenance/clean-storyboards", a.userOnly(a.adminOnly(a.handleCleanStoryboards()))).Methods("DELETE") apiRouter.HandleFunc("/storyboards", a.userOnly(a.adminOnly(a.handleGetStoryboards()))).Methods("GET") diff --git a/internal/http/middleware.go b/internal/http/middleware.go index e30942e4..072bd35a 100644 --- a/internal/http/middleware.go +++ b/internal/http/middleware.go @@ -242,6 +242,15 @@ func (s *Service) orgUserOnly(h http.HandlerFunc) http.HandlerFunc { return } + _, err := s.OrganizationDataSvc.OrganizationGet(ctx, OrgID) + if err != nil && err.Error() == "ORGANIZATION_NOT_FOUND" { + s.Failure(w, r, http.StatusNotFound, Errorf(ENOTFOUND, "ORGANIZATION_NOT_FOUND")) + return + } else if err != nil { + s.Failure(w, r, http.StatusInternalServerError, Errorf(EINTERNAL, err.Error())) + return + } + var Role string if UserType != thunderdome.AdminUserType { var UserErr error @@ -296,92 +305,6 @@ func (s *Service) orgAdminOnly(h http.HandlerFunc) http.HandlerFunc { } } -// orgTeamOnly validates that the request was made by an user of the organization team (or organization) -func (s *Service) orgTeamOnly(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - ctx := r.Context() - UserID := ctx.Value(contextKeyUserID).(string) - UserType := ctx.Value(contextKeyUserType).(string) - OrgID := vars["orgId"] - idErr := validate.Var(OrgID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - TeamID := vars["teamId"] - idErr = validate.Var(TeamID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - - var OrgRole string - var TeamRole string - if UserType != thunderdome.AdminUserType { - var UserErr error - OrgRole, TeamRole, UserErr = s.OrganizationDataSvc.OrganizationTeamUserRole(ctx, UserID, OrgID, TeamID) - if UserErr != nil { - s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_USER")) - return - } - } else { - OrgRole = thunderdome.AdminUserType - TeamRole = thunderdome.AdminUserType - } - - ctx = context.WithValue(ctx, contextKeyOrgRole, OrgRole) - ctx = context.WithValue(ctx, contextKeyTeamRole, TeamRole) - - h(w, r.WithContext(ctx)) - } -} - -// orgTeamAdminOnly validates that the request was made by an ADMIN of the organization team (or organization) -func (s *Service) orgTeamAdminOnly(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - ctx := r.Context() - UserID := ctx.Value(contextKeyUserID).(string) - UserType := ctx.Value(contextKeyUserType).(string) - OrgID := vars["orgId"] - idErr := validate.Var(OrgID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - TeamID := vars["teamId"] - idErr = validate.Var(TeamID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - - var OrgRole string - var TeamRole string - if UserType != thunderdome.AdminUserType { - var UserErr error - OrgRole, TeamRole, UserErr := s.OrganizationDataSvc.OrganizationTeamUserRole(ctx, UserID, OrgID, TeamID) - if UserErr != nil { - s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_USER")) - return - } - if TeamRole != thunderdome.AdminUserType && OrgRole != thunderdome.AdminUserType { - s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_OR_ORGANIZATION_ADMIN")) - return - } - } else { - OrgRole = thunderdome.AdminUserType - TeamRole = thunderdome.AdminUserType - } - - ctx = context.WithValue(ctx, contextKeyOrgRole, OrgRole) - ctx = context.WithValue(ctx, contextKeyTeamRole, TeamRole) - - h(w, r.WithContext(ctx)) - } -} - // departmentUserOnly validates that the request was made by a valid user of the organization (with department role) func (s *Service) departmentUserOnly(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -408,6 +331,11 @@ func (s *Service) departmentUserOnly(h http.HandlerFunc) http.HandlerFunc { var UserErr error OrgRole, DepartmentRole, UserErr = s.OrganizationDataSvc.DepartmentUserRole(ctx, UserID, OrgID, DepartmentID) if UserErr != nil { + s.Logger.Ctx(ctx).Warn("middleware departmentUserOnly REQUIRES_DEPARTMENT_USER", + zap.Error(UserErr), + zap.String("user_id", UserID), + zap.String("org_id", OrgID), + zap.String("department_id", DepartmentID)) s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_DEPARTMENT_USER")) return } @@ -468,112 +396,8 @@ func (s *Service) departmentAdminOnly(h http.HandlerFunc) http.HandlerFunc { } } -// departmentTeamUserOnly validates that the request was made by an user of the department team (or organization) -func (s *Service) departmentTeamUserOnly(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - ctx := r.Context() - UserID := ctx.Value(contextKeyUserID).(string) - UserType := ctx.Value(contextKeyUserType).(string) - OrgID := vars["orgId"] - idErr := validate.Var(OrgID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - DepartmentID := vars["departmentId"] - idErr = validate.Var(DepartmentID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - TeamID := vars["teamId"] - idErr = validate.Var(TeamID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - - var OrgRole string - var DepartmentRole string - var TeamRole string - if UserType != thunderdome.AdminUserType { - var UserErr error - OrgRole, DepartmentRole, TeamRole, UserErr = s.OrganizationDataSvc.DepartmentTeamUserRole(ctx, UserID, OrgID, DepartmentID, TeamID) - if UserErr != nil { - s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_USER")) - return - } - } else { - OrgRole = thunderdome.AdminUserType - DepartmentRole = thunderdome.AdminUserType - TeamRole = thunderdome.AdminUserType - } - - ctx = context.WithValue(ctx, contextKeyOrgRole, OrgRole) - ctx = context.WithValue(ctx, contextKeyDepartmentRole, DepartmentRole) - ctx = context.WithValue(ctx, contextKeyTeamRole, TeamRole) - - h(w, r.WithContext(ctx)) - } -} - -// departmentTeamAdminOnly validates that the request was made by an ADMIN of the department team (or organization) -func (s *Service) departmentTeamAdminOnly(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - ctx := r.Context() - UserID := ctx.Value(contextKeyUserID).(string) - UserType := ctx.Value(contextKeyUserType).(string) - OrgID := vars["orgId"] - idErr := validate.Var(OrgID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - DepartmentID := vars["departmentId"] - idErr = validate.Var(DepartmentID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - TeamID := vars["teamId"] - idErr = validate.Var(TeamID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } - - var OrgRole string - var DepartmentRole string - var TeamRole string - if UserType != thunderdome.AdminUserType { - var UserErr error - OrgRole, DepartmentRole, TeamRole, UserErr = s.OrganizationDataSvc.DepartmentTeamUserRole(ctx, UserID, OrgID, DepartmentID, TeamID) - if UserErr != nil { - s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_USER")) - return - } - - if TeamRole != thunderdome.AdminUserType && DepartmentRole != thunderdome.AdminUserType && OrgRole != thunderdome.AdminUserType { - s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_OR_DEPARTMENT_OR_ORGANIZATION_ADMIN")) - return - } - } else { - OrgRole = thunderdome.AdminUserType - DepartmentRole = thunderdome.AdminUserType - TeamRole = thunderdome.AdminUserType - } - - ctx = context.WithValue(ctx, contextKeyOrgRole, OrgRole) - ctx = context.WithValue(ctx, contextKeyDepartmentRole, DepartmentRole) - ctx = context.WithValue(ctx, contextKeyTeamRole, TeamRole) - - h(w, r.WithContext(ctx)) - } -} - // teamUserOnly validates that the request was made by a valid user of the team +// with bypass for global admins, and if associated to team department and/or organization admins func (s *Service) teamUserOnly(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -587,56 +411,49 @@ func (s *Service) teamUserOnly(h http.HandlerFunc) http.HandlerFunc { return } - var Role string - if UserType != thunderdome.AdminUserType { - var UserErr error - Role, UserErr = s.TeamDataSvc.TeamUserRole(ctx, UserID, TeamID) - if UserType != thunderdome.AdminUserType && UserErr != nil { - s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_USER")) - return - } - } else { - Role = thunderdome.AdminUserType + Roles, err := s.TeamDataSvc.TeamUserRoles(ctx, UserID, TeamID) + if err != nil && err.Error() == "TEAM_NOT_FOUND" { + s.Logger.Ctx(ctx).Warn("middleware teamUserOnly TEAM_NOT_FOUND", + zap.Any("team_user_roles", Roles), + zap.String("user_type", UserType)) + s.Failure(w, r, http.StatusNotFound, Errorf(ENOTFOUND, "TEAM_NOT_FOUND")) + return + } else if err != nil || (UserType != thunderdome.AdminUserType && + Roles.AssociationLevel != "TEAM" && + (Roles.DepartmentRole == nil || (Roles.DepartmentRole != nil && *Roles.DepartmentRole != thunderdome.AdminUserType)) && + (Roles.OrganizationRole == nil || (Roles.OrganizationRole != nil && *Roles.OrganizationRole != thunderdome.AdminUserType))) { + s.Logger.Ctx(ctx).Warn("middleware teamUserOnly REQUIRES_TEAM_USER", + zap.Any("team_user_roles", Roles), + zap.String("user_type", UserType)) + s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_USER")) + return } - ctx = context.WithValue(ctx, contextKeyTeamRole, Role) + ctx = context.WithValue(ctx, contextKeyUserTeamRoles, Roles) h(w, r.WithContext(ctx)) } } // teamAdminOnly validates that the request was made by an ADMIN of the team +// or an ADMIN of the team's parent entities if associated (department or organization) func (s *Service) teamAdminOnly(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) ctx := r.Context() - UserID := ctx.Value(contextKeyUserID).(string) UserType := ctx.Value(contextKeyUserType).(string) - TeamID := vars["teamId"] - idErr := validate.Var(TeamID, "required,uuid") - if idErr != nil { - s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) - return - } + TeamUserRoles := ctx.Value(contextKeyUserTeamRoles).(*thunderdome.UserTeamRoleInfo) - var Role string - if UserType != thunderdome.AdminUserType { - var UserErr error - Role, UserErr = s.TeamDataSvc.TeamUserRole(ctx, UserID, TeamID) - if UserErr != nil { - s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_USER")) - return - } - if Role != thunderdome.AdminUserType { - s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_ADMIN")) - return - } - } else { - Role = thunderdome.AdminUserType + if UserType != thunderdome.AdminUserType && + (TeamUserRoles.TeamRole == nil || *TeamUserRoles.TeamRole != thunderdome.AdminUserType) && + (TeamUserRoles.DepartmentRole == nil || *TeamUserRoles.DepartmentRole != thunderdome.AdminUserType) && + (TeamUserRoles.OrganizationRole == nil || *TeamUserRoles.OrganizationRole != thunderdome.AdminUserType) { + s.Logger.Ctx(ctx).Warn("middleware teamAdminOnly REQUIRES_TEAM_ADMIN", + zap.Any("team_user_roles", TeamUserRoles), + zap.String("user_type", UserType)) + s.Failure(w, r, http.StatusForbidden, Errorf(EUNAUTHORIZED, "REQUIRES_TEAM_ADMIN")) + return } - ctx = context.WithValue(ctx, contextKeyTeamRole, Role) - h(w, r.WithContext(ctx)) } } diff --git a/internal/http/middleware_test.go b/internal/http/middleware_test.go new file mode 100644 index 00000000..cdccbcfc --- /dev/null +++ b/internal/http/middleware_test.go @@ -0,0 +1,429 @@ +package http + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/StevenWeathers/thunderdome-planning-poker/thunderdome" + "github.com/uptrace/opentelemetry-go-extra/otelzap" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" +) + +func ptr[T any](v T) *T { + return &v +} + +// MockTeamDataSvc is a mock implementation of the TeamDataSvc +type MockTeamDataSvc struct { + mock.Mock +} + +func (m *MockTeamDataSvc) TeamUserRole(ctx context.Context, UserID string, TeamID string) (string, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamGet(ctx context.Context, TeamID string) (*thunderdome.Team, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamListByUser(ctx context.Context, UserID string, Limit int, Offset int) []*thunderdome.UserTeam { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamListByUserNonOrg(ctx context.Context, UserID string, Limit int, Offset int) []*thunderdome.UserTeam { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamCreate(ctx context.Context, UserID string, TeamName string) (*thunderdome.Team, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamUpdate(ctx context.Context, TeamId string, TeamName string) (*thunderdome.Team, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamAddUser(ctx context.Context, TeamID string, UserID string, Role string) (string, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamUserList(ctx context.Context, TeamID string, Limit int, Offset int) ([]*thunderdome.TeamUser, int, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamUpdateUser(ctx context.Context, TeamID string, UserID string, Role string) (string, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamRemoveUser(ctx context.Context, TeamID string, UserID string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamInviteUser(ctx context.Context, TeamID string, Email string, Role string) (string, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamUserGetInviteByID(ctx context.Context, InviteID string) (thunderdome.TeamUserInvite, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamDeleteUserInvite(ctx context.Context, InviteID string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamGetUserInvites(ctx context.Context, teamId string) ([]thunderdome.TeamUserInvite, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamPokerList(ctx context.Context, TeamID string, Limit int, Offset int) []*thunderdome.Poker { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamAddPoker(ctx context.Context, TeamID string, PokerID string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamRemovePoker(ctx context.Context, TeamID string, PokerID string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamDelete(ctx context.Context, TeamID string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamRetroList(ctx context.Context, TeamID string, Limit int, Offset int) []*thunderdome.Retro { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamAddRetro(ctx context.Context, TeamID string, RetroID string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamRemoveRetro(ctx context.Context, TeamID string, RetroID string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamStoryboardList(ctx context.Context, TeamID string, Limit int, Offset int) []*thunderdome.Storyboard { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamAddStoryboard(ctx context.Context, TeamID string, StoryboardID string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamRemoveStoryboard(ctx context.Context, TeamID string, StoryboardID string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamList(ctx context.Context, Limit int, Offset int) ([]*thunderdome.Team, int) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamIsSubscribed(ctx context.Context, TeamID string) (bool, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) GetTeamMetrics(ctx context.Context, teamID string) (*thunderdome.TeamMetrics, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTeamDataSvc) TeamUserRoles(ctx context.Context, userID, teamID string) (*thunderdome.UserTeamRoleInfo, error) { + args := m.Called(ctx, userID, teamID) + utr := args.Get(0).(thunderdome.UserTeamRoleInfo) + return &utr, args.Error(1) +} + +// MockLogger is a mock implementation of the Logger +type MockLogger struct { + mock.Mock +} + +func (m *MockLogger) Ctx(ctx context.Context) *zap.Logger { + args := m.Called(ctx) + return args.Get(0).(*zap.Logger) +} + +func TestTeamUserOnly(t *testing.T) { + tests := []struct { + name string + userID string + userType string + teamID string + setupMocks func(*MockTeamDataSvc, *MockLogger) + expectedStatus int + }{ + { + name: "Valid team user", + userID: "2d6176c8-50d6-4963-8172-2c20ca5022a3", + userType: "REGISTERED", + teamID: "128ee064-62ca-43b2-9fca-9c1089c89bd2", + setupMocks: func(mtds *MockTeamDataSvc, ml *MockLogger) { + mtds.On( + "TeamUserRoles", + mock.Anything, + "2d6176c8-50d6-4963-8172-2c20ca5022a3", + "128ee064-62ca-43b2-9fca-9c1089c89bd2", + ).Return(thunderdome.UserTeamRoleInfo{AssociationLevel: "TEAM"}, nil) + ml.On("Ctx", mock.Anything).Return(zap.NewNop()) + }, + expectedStatus: http.StatusOK, + }, + { + name: "Global admin bypass", + userID: "306cf097-d75d-4ec0-8960-a5c2914b28b9", + userType: thunderdome.AdminUserType, + teamID: "67cdc2f5-b4b0-444b-af0b-ae686e4cf9c8", + setupMocks: func(mtds *MockTeamDataSvc, ml *MockLogger) { + mtds.On( + "TeamUserRoles", + mock.Anything, + "306cf097-d75d-4ec0-8960-a5c2914b28b9", + "67cdc2f5-b4b0-444b-af0b-ae686e4cf9c8", + ).Return(thunderdome.UserTeamRoleInfo{}, nil) + ml.On("Ctx", mock.Anything).Return(zap.NewNop()) + }, + expectedStatus: http.StatusOK, + }, + { + name: "Department admin", + userID: "6a12ef8c-1140-4faa-a505-22910a7593f9", + userType: "REGISTERED", + teamID: "7d4d0a17-cb20-4499-bc7f-ecaf2e77c15a", + setupMocks: func(mtds *MockTeamDataSvc, ml *MockLogger) { + deptRole := thunderdome.AdminUserType + mtds.On( + "TeamUserRoles", + mock.Anything, + "6a12ef8c-1140-4faa-a505-22910a7593f9", + "7d4d0a17-cb20-4499-bc7f-ecaf2e77c15a", + ).Return(thunderdome.UserTeamRoleInfo{DepartmentRole: &deptRole}, nil) + ml.On("Ctx", mock.Anything).Return(zap.NewNop()) + }, + expectedStatus: http.StatusOK, + }, + { + name: "Organization admin", + userID: "31c8521e-2e68-4898-b3cf-e919cf80dbe2", + userType: "REGISTERED", + teamID: "0ea230df-b5fe-47ae-a473-5153004eebdd", + setupMocks: func(mtds *MockTeamDataSvc, ml *MockLogger) { + orgRole := thunderdome.AdminUserType + mtds.On( + "TeamUserRoles", + mock.Anything, + "31c8521e-2e68-4898-b3cf-e919cf80dbe2", + "0ea230df-b5fe-47ae-a473-5153004eebdd", + ).Return(thunderdome.UserTeamRoleInfo{OrganizationRole: &orgRole}, nil) + ml.On("Ctx", mock.Anything).Return(zap.NewNop()) + }, + expectedStatus: http.StatusOK, + }, + { + name: "Invalid team ID", + userID: "1353a056-d239-41e1-ad1a-b3f0777e6c3a", + userType: "REGISTERED", + teamID: "invalid-team-id", + setupMocks: func(mtds *MockTeamDataSvc, ml *MockLogger) { + ml.On("Ctx", mock.Anything).Return(zap.NewNop()) + }, + expectedStatus: http.StatusBadRequest, + }, + { + name: "Team not found", + userID: "1b853ef6-2c28-4c8e-ac29-a9d4827774fe", + userType: "REGISTERED", + teamID: "e52b9251-4722-4b4d-8f97-204ce7e51eec", + setupMocks: func(mtds *MockTeamDataSvc, ml *MockLogger) { + mtds.On( + "TeamUserRoles", + mock.Anything, + "1b853ef6-2c28-4c8e-ac29-a9d4827774fe", + "e52b9251-4722-4b4d-8f97-204ce7e51eec", + ).Return(thunderdome.UserTeamRoleInfo{}, fmt.Errorf("TEAM_NOT_FOUND")) + ml.On("Ctx", mock.Anything).Return(zap.NewNop()) + }, + expectedStatus: http.StatusNotFound, + }, + { + name: "Unauthorized user", + userID: "a805def1-e1fa-42a9-b5f6-ee338799fa77", + userType: "REGISTERED", + teamID: "a805def1-e1fa-42a9-b5f6-ee338799fa77", + setupMocks: func(mtds *MockTeamDataSvc, ml *MockLogger) { + mtds.On("TeamUserRoles", + mock.Anything, + "a805def1-e1fa-42a9-b5f6-ee338799fa77", + "a805def1-e1fa-42a9-b5f6-ee338799fa77", + ).Return(thunderdome.UserTeamRoleInfo{}, nil) + ml.On("Ctx", mock.Anything).Return(zap.NewNop()) + }, + expectedStatus: http.StatusForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockTeamDataSvc := new(MockTeamDataSvc) + mockLogger := new(MockLogger) + + tt.setupMocks(mockTeamDataSvc, mockLogger) + + s := &Service{ + TeamDataSvc: mockTeamDataSvc, + Logger: otelzap.New(mockLogger.Ctx(context.Background())), + } + + handler := s.teamUserOnly(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + req, _ := http.NewRequest("GET", "/teams/"+tt.teamID, nil) + req = mux.SetURLVars(req, map[string]string{"teamId": tt.teamID}) + req = req.WithContext(context.WithValue(req.Context(), contextKeyUserID, tt.userID)) + req = req.WithContext(context.WithValue(req.Context(), contextKeyUserType, tt.userType)) + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.Equal(t, tt.expectedStatus, rr.Code) + + mockTeamDataSvc.AssertExpectations(t) + mockLogger.AssertExpectations(t) + }) + } +} + +func TestTeamAdminOnly(t *testing.T) { + tests := []struct { + name string + userType string + teamRole *string + departmentRole *string + organizationRole *string + setupMocks func(*MockLogger) + expectedStatus int + }{ + { + name: "Global Admin", + userType: thunderdome.AdminUserType, + teamRole: nil, + departmentRole: nil, + organizationRole: nil, + expectedStatus: http.StatusOK, + }, + { + name: "Team Admin", + userType: thunderdome.RegisteredUserType, + teamRole: ptr(thunderdome.AdminUserType), + departmentRole: nil, + organizationRole: nil, + expectedStatus: http.StatusOK, + }, + { + name: "Department Admin", + userType: thunderdome.RegisteredUserType, + teamRole: nil, + departmentRole: ptr(thunderdome.AdminUserType), + organizationRole: nil, + expectedStatus: http.StatusOK, + }, + { + name: "Organization Admin", + userType: thunderdome.RegisteredUserType, + teamRole: nil, + departmentRole: nil, + organizationRole: ptr(thunderdome.AdminUserType), + expectedStatus: http.StatusOK, + }, + { + name: "Non-Admin User", + userType: thunderdome.RegisteredUserType, + teamRole: ptr("MEMBER"), + departmentRole: nil, + organizationRole: nil, + expectedStatus: http.StatusForbidden, + }, + { + name: "No Roles", + userType: thunderdome.RegisteredUserType, + teamRole: nil, + departmentRole: nil, + organizationRole: nil, + expectedStatus: http.StatusForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockLogger := new(MockLogger) + + mockLogger.On("Ctx", mock.Anything).Return(zap.NewNop()) + + // Create a test service + s := &Service{ + Logger: otelzap.New(mockLogger.Ctx(context.Background())), + } + + // Mock handler + mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + // Create a new request + req, err := http.NewRequest("GET", "/test", nil) + assert.NoError(t, err) + + // Set up the context with user type and roles + ctx := context.WithValue(req.Context(), contextKeyUserType, tt.userType) + ctx = context.WithValue(ctx, contextKeyUserTeamRoles, &thunderdome.UserTeamRoleInfo{ + TeamRole: tt.teamRole, + DepartmentRole: tt.departmentRole, + OrganizationRole: tt.organizationRole, + }) + req = req.WithContext(ctx) + + // Create a response recorder + rr := httptest.NewRecorder() + + // Call the middleware + handler := s.teamAdminOnly(mockHandler) + handler.ServeHTTP(rr, req) + + // Check the status code + assert.Equal(t, tt.expectedStatus, rr.Code) + }) + } +} diff --git a/internal/http/organization.go b/internal/http/organization.go index 75faa24f..a6e01f9f 100644 --- a/internal/http/organization.go +++ b/internal/http/organization.go @@ -563,8 +563,16 @@ func (s *Service) handleGetOrganizationTeamByUser() http.HandlerFunc { return } ctx := r.Context() - OrgRole := ctx.Value(contextKeyOrgRole).(string) - TeamRole := ctx.Value(contextKeyTeamRole).(string) + TeamUserRoles := ctx.Value(contextKeyUserTeamRoles).(*thunderdome.UserTeamRoleInfo) + var emptyRole = "" + OrgRole := TeamUserRoles.OrganizationRole + if OrgRole == nil { + OrgRole = &emptyRole + } + TeamRole := TeamUserRoles.TeamRole + if TeamRole == nil { + TeamRole = &emptyRole + } SessionUserID := ctx.Value(contextKeyUserID).(string) vars := mux.Vars(r) OrgID := vars["orgId"] @@ -584,8 +592,8 @@ func (s *Service) handleGetOrganizationTeamByUser() http.HandlerFunc { if err != nil { s.Logger.Ctx(ctx).Error( "handleGetOrganizationTeamByUser error", zap.Error(err), zap.String("organization_id", OrgID), - zap.String("session_user_id", SessionUserID), zap.String("organization_role", OrgRole), - zap.String("team_role", TeamRole), zap.String("team_id", TeamID)) + zap.String("session_user_id", SessionUserID), zap.String("organization_role", *OrgRole), + zap.String("team_role", *TeamRole), zap.String("team_id", TeamID)) s.Failure(w, r, http.StatusInternalServerError, err) return } @@ -594,8 +602,8 @@ func (s *Service) handleGetOrganizationTeamByUser() http.HandlerFunc { if err != nil { s.Logger.Ctx(ctx).Error( "handleGetOrganizationTeamByUser error", zap.Error(err), zap.String("organization_id", OrgID), - zap.String("session_user_id", SessionUserID), zap.String("organization_role", OrgRole), - zap.String("team_role", TeamRole), zap.String("team_id", TeamID)) + zap.String("session_user_id", SessionUserID), zap.String("organization_role", *OrgRole), + zap.String("team_role", *TeamRole), zap.String("team_id", TeamID)) s.Failure(w, r, http.StatusInternalServerError, err) return } @@ -603,8 +611,8 @@ func (s *Service) handleGetOrganizationTeamByUser() http.HandlerFunc { result := &orgTeamResponse{ Organization: Organization, Team: Team, - OrganizationRole: OrgRole, - TeamRole: TeamRole, + OrganizationRole: *OrgRole, + TeamRole: *TeamRole, } s.Success(w, r, http.StatusOK, result, nil) diff --git a/internal/http/poker/poker.go b/internal/http/poker/poker.go index ac1469ba..0ab4834c 100644 --- a/internal/http/poker/poker.go +++ b/internal/http/poker/poker.go @@ -39,6 +39,10 @@ func (c *Config) PongWait() time.Duration { return time.Duration(c.PongWaitSec) * time.Second } +type AuthDataSvc interface { + GetSessionUser(ctx context.Context, SessionId string) (*thunderdome.User, error) +} + // Service provides battle service type Service struct { config Config @@ -47,7 +51,7 @@ type Service struct { validateUserCookie func(w http.ResponseWriter, r *http.Request) (string, error) eventHandlers map[string]func(context.Context, string, string, string) ([]byte, error, bool) UserService thunderdome.UserDataSvc - AuthService thunderdome.AuthDataSvc + AuthService AuthDataSvc BattleService thunderdome.PokerDataSvc } @@ -56,7 +60,7 @@ func New( config Config, logger *otelzap.Logger, validateSessionCookie func(w http.ResponseWriter, r *http.Request) (string, error), validateUserCookie func(w http.ResponseWriter, r *http.Request) (string, error), - userService thunderdome.UserDataSvc, authService thunderdome.AuthDataSvc, + userService thunderdome.UserDataSvc, authService AuthDataSvc, battleService thunderdome.PokerDataSvc, ) *Service { b := &Service{ diff --git a/internal/http/retro/retro.go b/internal/http/retro/retro.go index 604676a8..d18c4d38 100644 --- a/internal/http/retro/retro.go +++ b/internal/http/retro/retro.go @@ -38,6 +38,10 @@ func (c *Config) PongWait() time.Duration { return time.Duration(c.PongWaitSec) * time.Second } +type AuthDataSvc interface { + GetSessionUser(ctx context.Context, SessionId string) (*thunderdome.User, error) +} + // Service provides retro service type Service struct { config Config @@ -46,7 +50,7 @@ type Service struct { validateUserCookie func(w http.ResponseWriter, r *http.Request) (string, error) eventHandlers map[string]func(context.Context, string, string, string) ([]byte, error, bool) UserService thunderdome.UserDataSvc - AuthService thunderdome.AuthDataSvc + AuthService AuthDataSvc RetroService thunderdome.RetroDataSvc TemplateService thunderdome.RetroTemplateDataSvc EmailService thunderdome.EmailService @@ -58,7 +62,7 @@ func New( logger *otelzap.Logger, validateSessionCookie func(w http.ResponseWriter, r *http.Request) (string, error), validateUserCookie func(w http.ResponseWriter, r *http.Request) (string, error), - userService thunderdome.UserDataSvc, authService thunderdome.AuthDataSvc, + userService thunderdome.UserDataSvc, authService AuthDataSvc, retroService thunderdome.RetroDataSvc, templateService thunderdome.RetroTemplateDataSvc, emailService thunderdome.EmailService, ) *Service { diff --git a/internal/http/storyboard/storyboard.go b/internal/http/storyboard/storyboard.go index 0fe9f0da..9345da97 100644 --- a/internal/http/storyboard/storyboard.go +++ b/internal/http/storyboard/storyboard.go @@ -38,6 +38,10 @@ func (c *Config) PongWait() time.Duration { return time.Duration(c.PongWaitSec) * time.Second } +type AuthDataSvc interface { + GetSessionUser(ctx context.Context, SessionId string) (*thunderdome.User, error) +} + // Service provides storyboard service type Service struct { config Config @@ -46,7 +50,7 @@ type Service struct { ValidateUserCookie func(w http.ResponseWriter, r *http.Request) (string, error) EventHandlers map[string]func(context.Context, string, string, string) ([]byte, error, bool) UserService thunderdome.UserDataSvc - AuthService thunderdome.AuthDataSvc + AuthService AuthDataSvc StoryboardService thunderdome.StoryboardDataSvc } @@ -56,7 +60,7 @@ func New( logger *otelzap.Logger, validateSessionCookie func(w http.ResponseWriter, r *http.Request) (string, error), validateUserCookie func(w http.ResponseWriter, r *http.Request) (string, error), - userService thunderdome.UserDataSvc, authService thunderdome.AuthDataSvc, + userService thunderdome.UserDataSvc, authService AuthDataSvc, storyboardService thunderdome.StoryboardDataSvc, ) *Service { sb := &Service{ diff --git a/internal/http/team.go b/internal/http/team.go index b0713b25..cf3aacbf 100644 --- a/internal/http/team.go +++ b/internal/http/team.go @@ -21,7 +21,7 @@ type teamResponse struct { TeamRole string `json:"teamRole"` } -// handleGetTeamByUser gets an team with user role +// handleGetTeamByUser gets a team with user role // @Summary Get Team // @Description Get a team with user role // @Tags team @@ -35,6 +35,7 @@ func (s *Service) handleGetTeamByUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() SessionUserID := ctx.Value(contextKeyUserID).(string) + UserTeamRoles := ctx.Value(contextKeyUserTeamRoles).(*thunderdome.UserTeamRoleInfo) vars := mux.Vars(r) TeamID := vars["teamId"] idErr := validate.Var(TeamID, "required,uuid") @@ -42,20 +43,27 @@ func (s *Service) handleGetTeamByUser() http.HandlerFunc { s.Failure(w, r, http.StatusBadRequest, Errorf(EINVALID, idErr.Error())) return } - TeamRole := r.Context().Value(contextKeyTeamRole).(string) + // Workaround until v5 refactor to replace meta's with consistency + TeamRole := UserTeamRoles.TeamRole + if TeamRole == nil { + var adminStr string + TeamRole = &adminStr + } Team, err := s.TeamDataSvc.TeamGet(ctx, TeamID) if err != nil { s.Logger.Ctx(ctx).Error( - "handleGetTeamByUser error", zap.Error(err), zap.String("team_id", TeamID), - zap.String("session_user_id", SessionUserID), zap.String("team_role", TeamRole)) + "handleGetTeamByUser error", zap.Error(err), + zap.String("team_id", TeamID), + zap.String("session_user_id", SessionUserID), + zap.String("team_role", *TeamRole)) s.Failure(w, r, http.StatusInternalServerError, err) return } result := &teamResponse{ Team: Team, - TeamRole: TeamRole, + TeamRole: *TeamRole, } s.Success(w, r, http.StatusOK, result, nil) diff --git a/internal/http/types.go b/internal/http/types.go index be1e1cf2..942252cb 100644 --- a/internal/http/types.go +++ b/internal/http/types.go @@ -1,7 +1,9 @@ package http import ( - "github.com/StevenWeathers/thunderdome-planning-poker/internal/cookie" + "context" + "net/http" + "github.com/StevenWeathers/thunderdome-planning-poker/internal/webhook/subscription" "github.com/StevenWeathers/thunderdome-planning-poker/thunderdome" "github.com/go-playground/validator/v10" @@ -13,9 +15,9 @@ const ( contextKeyUserID contextKey = "userId" contextKeyUserType contextKey = "userType" apiKeyHeaderName string = "X-API-Key" + contextKeyUserTeamRoles contextKey = "userTeamRoles" contextKeyOrgRole contextKey = "orgRole" contextKeyDepartmentRole contextKey = "departmentRole" - contextKeyTeamRole contextKey = "teamRole" ) var validate *validator.Validate @@ -102,23 +104,23 @@ type Config struct { type Service struct { Config *Config - Cookie *cookie.Cookie + Cookie CookieManager UIConfig thunderdome.UIConfig Router *mux.Router Email thunderdome.EmailService Logger *otelzap.Logger UserDataSvc thunderdome.UserDataSvc - ApiKeyDataSvc thunderdome.APIKeyDataSvc - AlertDataSvc thunderdome.AlertDataSvc - AuthDataSvc thunderdome.AuthDataSvc + ApiKeyDataSvc APIKeyDataSvc + AlertDataSvc AlertDataSvc + AuthDataSvc AuthDataSvc PokerDataSvc thunderdome.PokerDataSvc - CheckinDataSvc thunderdome.CheckinDataSvc + CheckinDataSvc CheckinDataSvc RetroDataSvc thunderdome.RetroDataSvc StoryboardDataSvc thunderdome.StoryboardDataSvc - TeamDataSvc thunderdome.TeamDataSvc - OrganizationDataSvc thunderdome.OrganizationDataSvc - AdminDataSvc thunderdome.AdminDataSvc - JiraDataSvc thunderdome.JiraDataSvc + TeamDataSvc TeamDataSvc + OrganizationDataSvc OrganizationDataSvc + AdminDataSvc AdminDataSvc + JiraDataSvc JiraDataSvc SubscriptionDataSvc thunderdome.SubscriptionDataSvc RetroTemplateDataSvc thunderdome.RetroTemplateDataSvc SubscriptionSvc *subscription.Service @@ -140,3 +142,152 @@ type pagination struct { } type contextKey string + +type CookieManager interface { + CreateUserCookie(w http.ResponseWriter, UserID string) error + CreateSessionCookie(w http.ResponseWriter, SessionID string) error + CreateUserUICookie(w http.ResponseWriter, userUiCookie thunderdome.UserUICookie) error + ClearUserCookies(w http.ResponseWriter) + ValidateUserCookie(w http.ResponseWriter, r *http.Request) (string, error) + ValidateSessionCookie(w http.ResponseWriter, r *http.Request) (string, error) + CreateCookie(w http.ResponseWriter, cookieName string, value string, maxAge int) error + GetCookie(w http.ResponseWriter, r *http.Request, cookieName string) (string, error) + DeleteCookie(w http.ResponseWriter, cookieName string) + CreateAuthStateCookie(w http.ResponseWriter, state string) error + ValidateAuthStateCookie(w http.ResponseWriter, r *http.Request, state string) error + DeleteAuthStateCookie(w http.ResponseWriter) error +} + +type AdminDataSvc interface { + GetAppStats(ctx context.Context) (*thunderdome.ApplicationStats, error) +} + +type AlertDataSvc interface { + GetActiveAlerts(ctx context.Context) []interface{} + AlertsList(ctx context.Context, Limit int, Offset int) ([]*thunderdome.Alert, int, error) + AlertsCreate(ctx context.Context, Name string, Type string, Content string, Active bool, AllowDismiss bool, RegisteredOnly bool) error + AlertsUpdate(ctx context.Context, ID string, Name string, Type string, Content string, Active bool, AllowDismiss bool, RegisteredOnly bool) error + AlertDelete(ctx context.Context, AlertID string) error +} + +type APIKeyDataSvc interface { + GenerateApiKey(ctx context.Context, UserID string, KeyName string) (*thunderdome.APIKey, error) + GetUserApiKeys(ctx context.Context, UserID string) ([]*thunderdome.APIKey, error) + GetApiKeyUser(ctx context.Context, APK string) (*thunderdome.User, error) + GetAPIKeys(ctx context.Context, Limit int, Offset int) []*thunderdome.UserAPIKey + UpdateUserApiKey(ctx context.Context, UserID string, KeyID string, Active bool) ([]*thunderdome.APIKey, error) + DeleteUserApiKey(ctx context.Context, UserID string, KeyID string) ([]*thunderdome.APIKey, error) +} + +type AuthDataSvc interface { + AuthUser(ctx context.Context, UserEmail string, UserPassword string) (*thunderdome.User, *thunderdome.Credential, string, error) + OauthCreateNonce(ctx context.Context) (string, error) + OauthValidateNonce(ctx context.Context, nonceId string) error + OauthAuthUser(ctx context.Context, provider string, sub string, email string, emailVerified bool, name string, pictureUrl string) (*thunderdome.User, string, error) + UserResetRequest(ctx context.Context, UserEmail string) (resetID string, UserName string, resetErr error) + UserResetPassword(ctx context.Context, ResetID string, UserPassword string) (UserName string, UserEmail string, resetErr error) + UserUpdatePassword(ctx context.Context, UserID string, UserPassword string) (Name string, Email string, resetErr error) + UserVerifyRequest(ctx context.Context, UserId string) (*thunderdome.User, string, error) + VerifyUserAccount(ctx context.Context, VerifyID string) error + MFASetupGenerate(email string) (string, string, error) + MFASetupValidate(ctx context.Context, UserID string, secret string, passcode string) error + MFARemove(ctx context.Context, UserID string) error + MFATokenValidate(ctx context.Context, SessionId string, passcode string) error + CreateSession(ctx context.Context, UserId string, enabled bool) (string, error) + EnableSession(ctx context.Context, SessionId string) error + GetSessionUser(ctx context.Context, SessionId string) (*thunderdome.User, error) + DeleteSession(ctx context.Context, SessionId string) error +} + +type CheckinDataSvc interface { + CheckinList(ctx context.Context, TeamId string, Date string, TimeZone string) ([]*thunderdome.TeamCheckin, error) + CheckinCreate(ctx context.Context, TeamId string, UserId string, Yesterday string, Today string, Blockers string, Discuss string, GoalsMet bool) error + CheckinUpdate(ctx context.Context, CheckinId string, Yesterday string, Today string, Blockers string, Discuss string, GoalsMet bool) error + CheckinDelete(ctx context.Context, CheckinId string) error + CheckinComment(ctx context.Context, TeamId string, CheckinId string, UserId string, Comment string) error + CheckinCommentEdit(ctx context.Context, TeamId string, UserId string, CommentId string, Comment string) error + CheckinCommentDelete(ctx context.Context, CommentId string) error + CheckinLastByUser(ctx context.Context, TeamId string, UserId string) (*thunderdome.TeamCheckin, error) +} + +type JiraDataSvc interface { + FindInstancesByUserId(ctx context.Context, userId string) ([]thunderdome.JiraInstance, error) + GetInstanceById(ctx context.Context, instanceId string) (thunderdome.JiraInstance, error) + CreateInstance(ctx context.Context, userId string, host string, clientMail string, accessToken string) (thunderdome.JiraInstance, error) + UpdateInstance(ctx context.Context, instanceId string, host string, clientMail string, accessToken string) (thunderdome.JiraInstance, error) + DeleteInstance(ctx context.Context, instanceId string) error +} + +type OrganizationDataSvc interface { + OrganizationGet(ctx context.Context, OrgID string) (*thunderdome.Organization, error) + OrganizationUserRole(ctx context.Context, UserID string, OrgID string) (string, error) + OrganizationListByUser(ctx context.Context, UserID string, Limit int, Offset int) []*thunderdome.UserOrganization + OrganizationCreate(ctx context.Context, UserID string, OrgName string) (*thunderdome.Organization, error) + OrganizationUpdate(ctx context.Context, OrgId string, OrgName string) (*thunderdome.Organization, error) + OrganizationUserList(ctx context.Context, OrgID string, Limit int, Offset int) []*thunderdome.OrganizationUser + OrganizationAddUser(ctx context.Context, OrgID string, UserID string, Role string) (string, error) + OrganizationUpsertUser(ctx context.Context, OrgID string, UserID string, Role string) (string, error) + OrganizationUpdateUser(ctx context.Context, OrgID string, UserID string, Role string) (string, error) + OrganizationRemoveUser(ctx context.Context, OrganizationID string, UserID string) error + OrganizationInviteUser(ctx context.Context, OrgID string, Email string, Role string) (string, error) + OrganizationUserGetInviteByID(ctx context.Context, InviteID string) (thunderdome.OrganizationUserInvite, error) + OrganizationDeleteUserInvite(ctx context.Context, InviteID string) error + OrganizationGetUserInvites(ctx context.Context, orgId string) ([]thunderdome.OrganizationUserInvite, error) + OrganizationTeamList(ctx context.Context, OrgID string, Limit int, Offset int) []*thunderdome.Team + OrganizationTeamCreate(ctx context.Context, OrgID string, TeamName string) (*thunderdome.Team, error) + OrganizationTeamUserRole(ctx context.Context, UserID string, OrgID string, TeamID string) (string, string, error) + OrganizationDelete(ctx context.Context, OrgID string) error + OrganizationList(ctx context.Context, Limit int, Offset int) []*thunderdome.Organization + OrganizationIsSubscribed(ctx context.Context, OrgID string) (bool, error) + GetOrganizationMetrics(ctx context.Context, organizationID string) (*thunderdome.OrganizationMetrics, error) + + DepartmentUserRole(ctx context.Context, UserID string, OrgID string, DepartmentID string) (string, string, error) + DepartmentGet(ctx context.Context, DepartmentID string) (*thunderdome.Department, error) + OrganizationDepartmentList(ctx context.Context, OrgID string, Limit int, Offset int) []*thunderdome.Department + DepartmentCreate(ctx context.Context, OrgID string, OrgName string) (*thunderdome.Department, error) + DepartmentUpdate(ctx context.Context, DeptId string, DeptName string) (*thunderdome.Department, error) + DepartmentTeamList(ctx context.Context, DepartmentID string, Limit int, Offset int) []*thunderdome.Team + DepartmentTeamCreate(ctx context.Context, DepartmentID string, TeamName string) (*thunderdome.Team, error) + DepartmentUserList(ctx context.Context, DepartmentID string, Limit int, Offset int) []*thunderdome.DepartmentUser + DepartmentAddUser(ctx context.Context, DepartmentID string, UserID string, Role string) (string, error) + DepartmentUpsertUser(ctx context.Context, DepartmentID string, UserID string, Role string) (string, error) + DepartmentUpdateUser(ctx context.Context, DepartmentID string, UserID string, Role string) (string, error) + DepartmentRemoveUser(ctx context.Context, DepartmentID string, UserID string) error + DepartmentTeamUserRole(ctx context.Context, UserID string, OrgID string, DepartmentID string, TeamID string) (string, string, string, error) + DepartmentDelete(ctx context.Context, DepartmentID string) error + DepartmentInviteUser(ctx context.Context, DeptID string, Email string, Role string) (string, error) + DepartmentUserGetInviteByID(ctx context.Context, InviteID string) (thunderdome.DepartmentUserInvite, error) + DepartmentDeleteUserInvite(ctx context.Context, InviteID string) error + DepartmentGetUserInvites(ctx context.Context, deptId string) ([]thunderdome.DepartmentUserInvite, error) +} + +type TeamDataSvc interface { + TeamUserRole(ctx context.Context, UserID string, TeamID string) (string, error) + TeamGet(ctx context.Context, TeamID string) (*thunderdome.Team, error) + TeamListByUser(ctx context.Context, UserID string, Limit int, Offset int) []*thunderdome.UserTeam + TeamListByUserNonOrg(ctx context.Context, UserID string, Limit int, Offset int) []*thunderdome.UserTeam + TeamCreate(ctx context.Context, UserID string, TeamName string) (*thunderdome.Team, error) + TeamUpdate(ctx context.Context, TeamId string, TeamName string) (*thunderdome.Team, error) + TeamAddUser(ctx context.Context, TeamID string, UserID string, Role string) (string, error) + TeamUserList(ctx context.Context, TeamID string, Limit int, Offset int) ([]*thunderdome.TeamUser, int, error) + TeamUpdateUser(ctx context.Context, TeamID string, UserID string, Role string) (string, error) + TeamRemoveUser(ctx context.Context, TeamID string, UserID string) error + TeamInviteUser(ctx context.Context, TeamID string, Email string, Role string) (string, error) + TeamUserGetInviteByID(ctx context.Context, InviteID string) (thunderdome.TeamUserInvite, error) + TeamDeleteUserInvite(ctx context.Context, InviteID string) error + TeamGetUserInvites(ctx context.Context, teamId string) ([]thunderdome.TeamUserInvite, error) + TeamPokerList(ctx context.Context, TeamID string, Limit int, Offset int) []*thunderdome.Poker + TeamAddPoker(ctx context.Context, TeamID string, PokerID string) error + TeamRemovePoker(ctx context.Context, TeamID string, PokerID string) error + TeamDelete(ctx context.Context, TeamID string) error + TeamRetroList(ctx context.Context, TeamID string, Limit int, Offset int) []*thunderdome.Retro + TeamAddRetro(ctx context.Context, TeamID string, RetroID string) error + TeamRemoveRetro(ctx context.Context, TeamID string, RetroID string) error + TeamStoryboardList(ctx context.Context, TeamID string, Limit int, Offset int) []*thunderdome.Storyboard + TeamAddStoryboard(ctx context.Context, TeamID string, StoryboardID string) error + TeamRemoveStoryboard(ctx context.Context, TeamID string, StoryboardID string) error + TeamList(ctx context.Context, Limit int, Offset int) ([]*thunderdome.Team, int) + TeamIsSubscribed(ctx context.Context, TeamID string) (bool, error) + GetTeamMetrics(ctx context.Context, teamID string) (*thunderdome.TeamMetrics, error) + TeamUserRoles(ctx context.Context, UserID string, TeamID string) (*thunderdome.UserTeamRoleInfo, error) +} diff --git a/internal/http/util.go b/internal/http/util.go index c5053a13..9761dde9 100644 --- a/internal/http/util.go +++ b/internal/http/util.go @@ -290,19 +290,32 @@ func (s *Service) authAndCreateUserHeader(ctx context.Context, username string, // isTeamUserOrAnAdmin determines if the request user is a team user // or team admin, or department admin (if applicable), or organization admin (if applicable), or application admin func isTeamUserOrAnAdmin(r *http.Request) bool { - UserType := r.Context().Value(contextKeyUserType).(string) - OrgRole := r.Context().Value(contextKeyOrgRole) - DepartmentRole := r.Context().Value(contextKeyDepartmentRole) - TeamRole := r.Context().Value(contextKeyTeamRole).(string) + ctx := r.Context() + TeamUserRoles := ctx.Value(contextKeyUserTeamRoles).(*thunderdome.UserTeamRoleInfo) + var emptyRole = "" + OrgRole := TeamUserRoles.OrganizationRole + if OrgRole == nil { + OrgRole = &emptyRole + } + DepartmentRole := TeamUserRoles.DepartmentRole + if DepartmentRole == nil { + DepartmentRole = &emptyRole + } + TeamRole := TeamUserRoles.TeamRole + if TeamRole == nil { + TeamRole = &emptyRole + } + + UserType := ctx.Value(contextKeyUserType).(string) var isAdmin = UserType == thunderdome.AdminUserType - if DepartmentRole != nil && DepartmentRole.(string) == thunderdome.AdminUserType { + if DepartmentRole != nil && *DepartmentRole == thunderdome.AdminUserType { isAdmin = true } - if OrgRole != nil && OrgRole.(string) == thunderdome.AdminUserType { + if OrgRole != nil && *OrgRole == thunderdome.AdminUserType { isAdmin = true } - return isAdmin || TeamRole != "" + return isAdmin || *TeamRole != "" } // get the index template from embedded filesystem diff --git a/internal/http/util_test.go b/internal/http/util_test.go index 896f6b5b..a753b06b 100644 --- a/internal/http/util_test.go +++ b/internal/http/util_test.go @@ -8,6 +8,7 @@ import ( func TestMain(m *testing.M) { validate = validator.New() + m.Run() } // TestValidUserAccount calls validateUserAccountWithPasswords with valid user inputs for name, email, password1, and password2 diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index bb861090..ab5f20f0 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -8,8 +8,6 @@ import ( "github.com/uptrace/opentelemetry-go-extra/otelzap" - "github.com/StevenWeathers/thunderdome-planning-poker/internal/cookie" - "github.com/coreos/go-oidc/v3/oidc" "github.com/google/uuid" "go.uber.org/zap" @@ -18,9 +16,9 @@ import ( func New( config Config, - cookie *cookie.Cookie, + cookie CookieManager, logger *otelzap.Logger, - authDataSvc thunderdome.AuthDataSvc, + authDataSvc AuthDataSvc, subscriptionDataSvc thunderdome.SubscriptionDataSvc, ctx context.Context, ) (*Service, error) { diff --git a/internal/oauth/type.go b/internal/oauth/type.go index e3278ae4..c9bfb5d4 100644 --- a/internal/oauth/type.go +++ b/internal/oauth/type.go @@ -1,7 +1,9 @@ package oauth import ( - "github.com/StevenWeathers/thunderdome-planning-poker/internal/cookie" + "context" + "net/http" + "github.com/StevenWeathers/thunderdome-planning-poker/thunderdome" "github.com/coreos/go-oidc/v3/oidc" "github.com/uptrace/opentelemetry-go-extra/otelzap" @@ -14,12 +16,33 @@ type Config struct { UIRedirectURL string } +type CookieManager interface { + CreateUserCookie(w http.ResponseWriter, UserID string) error + CreateSessionCookie(w http.ResponseWriter, SessionID string) error + CreateUserUICookie(w http.ResponseWriter, userUiCookie thunderdome.UserUICookie) error + ClearUserCookies(w http.ResponseWriter) + ValidateUserCookie(w http.ResponseWriter, r *http.Request) (string, error) + ValidateSessionCookie(w http.ResponseWriter, r *http.Request) (string, error) + CreateCookie(w http.ResponseWriter, cookieName string, value string, maxAge int) error + GetCookie(w http.ResponseWriter, r *http.Request, cookieName string) (string, error) + DeleteCookie(w http.ResponseWriter, cookieName string) + CreateAuthStateCookie(w http.ResponseWriter, state string) error + ValidateAuthStateCookie(w http.ResponseWriter, r *http.Request, state string) error + DeleteAuthStateCookie(w http.ResponseWriter) error +} + +type AuthDataSvc interface { + OauthCreateNonce(ctx context.Context) (string, error) + OauthValidateNonce(ctx context.Context, nonceId string) error + OauthAuthUser(ctx context.Context, provider string, sub string, email string, emailVerified bool, name string, pictureUrl string) (*thunderdome.User, string, error) +} + type Service struct { config Config - cookie *cookie.Cookie + cookie CookieManager oauth2Config *oauth2.Config logger *otelzap.Logger verifier *oidc.IDTokenVerifier - authDataSvc thunderdome.AuthDataSvc + authDataSvc AuthDataSvc subscriptionDataSvc thunderdome.SubscriptionDataSvc } diff --git a/thunderdome/admin.go b/thunderdome/admin.go index b69bca92..5f9115ed 100644 --- a/thunderdome/admin.go +++ b/thunderdome/admin.go @@ -1,7 +1,5 @@ package thunderdome -import "context" - // ApplicationStats includes counts of different data points of the application type ApplicationStats struct { UnregisteredCount int `json:"unregisteredUserCount"` @@ -39,7 +37,3 @@ type ApplicationStats struct { TeamRetroTemplateCount int `json:"teamRetroTemplateCount"` PublicRetroTemplateCount int `json:"publicRetroTemplateCount"` } - -type AdminDataSvc interface { - GetAppStats(ctx context.Context) (*ApplicationStats, error) -} diff --git a/thunderdome/alert.go b/thunderdome/alert.go index cc226427..fafc27ed 100644 --- a/thunderdome/alert.go +++ b/thunderdome/alert.go @@ -1,7 +1,6 @@ package thunderdome import ( - "context" "time" ) @@ -16,11 +15,3 @@ type Alert struct { CreatedDate time.Time `json:"createdDate" db:"created_date"` UpdatedDate time.Time `json:"updatedDate" db:"updated_date"` } - -type AlertDataSvc interface { - GetActiveAlerts(ctx context.Context) []interface{} - AlertsList(ctx context.Context, Limit int, Offset int) ([]*Alert, int, error) - AlertsCreate(ctx context.Context, Name string, Type string, Content string, Active bool, AllowDismiss bool, RegisteredOnly bool) error - AlertsUpdate(ctx context.Context, ID string, Name string, Type string, Content string, Active bool, AllowDismiss bool, RegisteredOnly bool) error - AlertDelete(ctx context.Context, AlertID string) error -} diff --git a/thunderdome/apikey.go b/thunderdome/apikey.go index eb06edb3..1d636f1f 100644 --- a/thunderdome/apikey.go +++ b/thunderdome/apikey.go @@ -1,7 +1,6 @@ package thunderdome import ( - "context" "time" ) @@ -30,12 +29,3 @@ type UserAPIKey struct { CreatedDate time.Time `json:"createdDate"` UpdatedDate time.Time `json:"updatedDate"` } - -type APIKeyDataSvc interface { - GenerateApiKey(ctx context.Context, UserID string, KeyName string) (*APIKey, error) - GetUserApiKeys(ctx context.Context, UserID string) ([]*APIKey, error) - GetApiKeyUser(ctx context.Context, APK string) (*User, error) - GetAPIKeys(ctx context.Context, Limit int, Offset int) []*UserAPIKey - UpdateUserApiKey(ctx context.Context, UserID string, KeyID string, Active bool) ([]*APIKey, error) - DeleteUserApiKey(ctx context.Context, UserID string, KeyID string) ([]*APIKey, error) -} diff --git a/thunderdome/auth.go b/thunderdome/auth.go index 6912f4a4..baef17b4 100644 --- a/thunderdome/auth.go +++ b/thunderdome/auth.go @@ -1,7 +1,6 @@ package thunderdome import ( - "context" "net/http" "time" ) @@ -37,23 +36,3 @@ type AuthProviderSvc interface { HandleOAuth2Redirect(w http.ResponseWriter, r *http.Request) HandleOAuth2Callback(w http.ResponseWriter, r *http.Request) } - -type AuthDataSvc interface { - AuthUser(ctx context.Context, UserEmail string, UserPassword string) (*User, *Credential, string, error) - OauthCreateNonce(ctx context.Context) (string, error) - OauthValidateNonce(ctx context.Context, nonceId string) error - OauthAuthUser(ctx context.Context, provider string, sub string, email string, emailVerified bool, name string, pictureUrl string) (*User, string, error) - UserResetRequest(ctx context.Context, UserEmail string) (resetID string, UserName string, resetErr error) - UserResetPassword(ctx context.Context, ResetID string, UserPassword string) (UserName string, UserEmail string, resetErr error) - UserUpdatePassword(ctx context.Context, UserID string, UserPassword string) (Name string, Email string, resetErr error) - UserVerifyRequest(ctx context.Context, UserId string) (*User, string, error) - VerifyUserAccount(ctx context.Context, VerifyID string) error - MFASetupGenerate(email string) (string, string, error) - MFASetupValidate(ctx context.Context, UserID string, secret string, passcode string) error - MFARemove(ctx context.Context, UserID string) error - MFATokenValidate(ctx context.Context, SessionId string, passcode string) error - CreateSession(ctx context.Context, UserId string, enabled bool) (string, error) - EnableSession(ctx context.Context, SessionId string) error - GetSessionUser(ctx context.Context, SessionId string) (*User, error) - DeleteSession(ctx context.Context, SessionId string) error -} diff --git a/thunderdome/checkin.go b/thunderdome/checkin.go index ab22ffc0..ec86a8d9 100644 --- a/thunderdome/checkin.go +++ b/thunderdome/checkin.go @@ -1,7 +1,5 @@ package thunderdome -import "context" - type TeamCheckin struct { Id string `json:"id"` User *TeamUser `json:"user"` @@ -24,14 +22,3 @@ type CheckinComment struct { CreateDate string `json:"created_date"` UpdatedDate string `json:"updated_date"` } - -type CheckinDataSvc interface { - CheckinList(ctx context.Context, TeamId string, Date string, TimeZone string) ([]*TeamCheckin, error) - CheckinCreate(ctx context.Context, TeamId string, UserId string, Yesterday string, Today string, Blockers string, Discuss string, GoalsMet bool) error - CheckinUpdate(ctx context.Context, CheckinId string, Yesterday string, Today string, Blockers string, Discuss string, GoalsMet bool) error - CheckinDelete(ctx context.Context, CheckinId string) error - CheckinComment(ctx context.Context, TeamId string, CheckinId string, UserId string, Comment string) error - CheckinCommentEdit(ctx context.Context, TeamId string, UserId string, CommentId string, Comment string) error - CheckinCommentDelete(ctx context.Context, CommentId string) error - CheckinLastByUser(ctx context.Context, TeamId string, UserId string) (*TeamCheckin, error) -} diff --git a/thunderdome/jira.go b/thunderdome/jira.go index 849423e1..9f79a0d7 100644 --- a/thunderdome/jira.go +++ b/thunderdome/jira.go @@ -1,7 +1,6 @@ package thunderdome import ( - "context" "time" ) @@ -14,11 +13,3 @@ type JiraInstance struct { CreatedDate time.Time `json:"created_date"` UpdatedDate time.Time `json:"updated_date"` } - -type JiraDataSvc interface { - FindInstancesByUserId(ctx context.Context, userId string) ([]JiraInstance, error) - GetInstanceById(ctx context.Context, instanceId string) (JiraInstance, error) - CreateInstance(ctx context.Context, userId string, host string, clientMail string, accessToken string) (JiraInstance, error) - UpdateInstance(ctx context.Context, instanceId string, host string, clientMail string, accessToken string) (JiraInstance, error) - DeleteInstance(ctx context.Context, instanceId string) error -} diff --git a/thunderdome/organization.go b/thunderdome/organization.go index 73d2f62c..9b88c0f3 100644 --- a/thunderdome/organization.go +++ b/thunderdome/organization.go @@ -1,7 +1,6 @@ package thunderdome import ( - "context" "time" ) @@ -84,46 +83,3 @@ type OrganizationMetrics struct { EstimationScaleCount int `json:"estimation_scale_count"` RetroTemplateCount int `json:"retro_template_count"` } - -type OrganizationDataSvc interface { - OrganizationGet(ctx context.Context, OrgID string) (*Organization, error) - OrganizationUserRole(ctx context.Context, UserID string, OrgID string) (string, error) - OrganizationListByUser(ctx context.Context, UserID string, Limit int, Offset int) []*UserOrganization - OrganizationCreate(ctx context.Context, UserID string, OrgName string) (*Organization, error) - OrganizationUpdate(ctx context.Context, OrgId string, OrgName string) (*Organization, error) - OrganizationUserList(ctx context.Context, OrgID string, Limit int, Offset int) []*OrganizationUser - OrganizationAddUser(ctx context.Context, OrgID string, UserID string, Role string) (string, error) - OrganizationUpsertUser(ctx context.Context, OrgID string, UserID string, Role string) (string, error) - OrganizationUpdateUser(ctx context.Context, OrgID string, UserID string, Role string) (string, error) - OrganizationRemoveUser(ctx context.Context, OrganizationID string, UserID string) error - OrganizationInviteUser(ctx context.Context, OrgID string, Email string, Role string) (string, error) - OrganizationUserGetInviteByID(ctx context.Context, InviteID string) (OrganizationUserInvite, error) - OrganizationDeleteUserInvite(ctx context.Context, InviteID string) error - OrganizationGetUserInvites(ctx context.Context, orgId string) ([]OrganizationUserInvite, error) - OrganizationTeamList(ctx context.Context, OrgID string, Limit int, Offset int) []*Team - OrganizationTeamCreate(ctx context.Context, OrgID string, TeamName string) (*Team, error) - OrganizationTeamUserRole(ctx context.Context, UserID string, OrgID string, TeamID string) (string, string, error) - OrganizationDelete(ctx context.Context, OrgID string) error - OrganizationList(ctx context.Context, Limit int, Offset int) []*Organization - OrganizationIsSubscribed(ctx context.Context, OrgID string) (bool, error) - GetOrganizationMetrics(ctx context.Context, organizationID string) (*OrganizationMetrics, error) - - DepartmentUserRole(ctx context.Context, UserID string, OrgID string, DepartmentID string) (string, string, error) - DepartmentGet(ctx context.Context, DepartmentID string) (*Department, error) - OrganizationDepartmentList(ctx context.Context, OrgID string, Limit int, Offset int) []*Department - DepartmentCreate(ctx context.Context, OrgID string, OrgName string) (*Department, error) - DepartmentUpdate(ctx context.Context, DeptId string, DeptName string) (*Department, error) - DepartmentTeamList(ctx context.Context, DepartmentID string, Limit int, Offset int) []*Team - DepartmentTeamCreate(ctx context.Context, DepartmentID string, TeamName string) (*Team, error) - DepartmentUserList(ctx context.Context, DepartmentID string, Limit int, Offset int) []*DepartmentUser - DepartmentAddUser(ctx context.Context, DepartmentID string, UserID string, Role string) (string, error) - DepartmentUpsertUser(ctx context.Context, DepartmentID string, UserID string, Role string) (string, error) - DepartmentUpdateUser(ctx context.Context, DepartmentID string, UserID string, Role string) (string, error) - DepartmentRemoveUser(ctx context.Context, DepartmentID string, UserID string) error - DepartmentTeamUserRole(ctx context.Context, UserID string, OrgID string, DepartmentID string, TeamID string) (string, string, string, error) - DepartmentDelete(ctx context.Context, DepartmentID string) error - DepartmentInviteUser(ctx context.Context, DeptID string, Email string, Role string) (string, error) - DepartmentUserGetInviteByID(ctx context.Context, InviteID string) (DepartmentUserInvite, error) - DepartmentDeleteUserInvite(ctx context.Context, InviteID string) error - DepartmentGetUserInvites(ctx context.Context, deptId string) ([]DepartmentUserInvite, error) -} diff --git a/thunderdome/team.go b/thunderdome/team.go index 1b356b47..6eba7270 100644 --- a/thunderdome/team.go +++ b/thunderdome/team.go @@ -1,7 +1,6 @@ package thunderdome import ( - "context" "time" ) @@ -56,32 +55,14 @@ type TeamMetrics struct { RetroTemplateCount int `json:"retro_template_count"` } -type TeamDataSvc interface { - TeamUserRole(ctx context.Context, UserID string, TeamID string) (string, error) - TeamGet(ctx context.Context, TeamID string) (*Team, error) - TeamListByUser(ctx context.Context, UserID string, Limit int, Offset int) []*UserTeam - TeamListByUserNonOrg(ctx context.Context, UserID string, Limit int, Offset int) []*UserTeam - TeamCreate(ctx context.Context, UserID string, TeamName string) (*Team, error) - TeamUpdate(ctx context.Context, TeamId string, TeamName string) (*Team, error) - TeamAddUser(ctx context.Context, TeamID string, UserID string, Role string) (string, error) - TeamUserList(ctx context.Context, TeamID string, Limit int, Offset int) ([]*TeamUser, int, error) - TeamUpdateUser(ctx context.Context, TeamID string, UserID string, Role string) (string, error) - TeamRemoveUser(ctx context.Context, TeamID string, UserID string) error - TeamInviteUser(ctx context.Context, TeamID string, Email string, Role string) (string, error) - TeamUserGetInviteByID(ctx context.Context, InviteID string) (TeamUserInvite, error) - TeamDeleteUserInvite(ctx context.Context, InviteID string) error - TeamGetUserInvites(ctx context.Context, teamId string) ([]TeamUserInvite, error) - TeamPokerList(ctx context.Context, TeamID string, Limit int, Offset int) []*Poker - TeamAddPoker(ctx context.Context, TeamID string, PokerID string) error - TeamRemovePoker(ctx context.Context, TeamID string, PokerID string) error - TeamDelete(ctx context.Context, TeamID string) error - TeamRetroList(ctx context.Context, TeamID string, Limit int, Offset int) []*Retro - TeamAddRetro(ctx context.Context, TeamID string, RetroID string) error - TeamRemoveRetro(ctx context.Context, TeamID string, RetroID string) error - TeamStoryboardList(ctx context.Context, TeamID string, Limit int, Offset int) []*Storyboard - TeamAddStoryboard(ctx context.Context, TeamID string, StoryboardID string) error - TeamRemoveStoryboard(ctx context.Context, TeamID string, StoryboardID string) error - TeamList(ctx context.Context, Limit int, Offset int) ([]*Team, int) - TeamIsSubscribed(ctx context.Context, TeamID string) (bool, error) - GetTeamMetrics(ctx context.Context, teamID string) (*TeamMetrics, error) +// UserTeamRoleInfo represents a team's structure and a user's roles (if any) for that team. +type UserTeamRoleInfo struct { + UserID string `db:"user_id" json:"userId"` + TeamID string `db:"team_id" json:"teamId"` + TeamRole *string `db:"team_role" json:"teamRole"` + DepartmentID *string `db:"department_id" json:"departmentId"` + DepartmentRole *string `db:"department_role" json:"departmentRole"` + OrganizationID *string `db:"organization_id" json:"organizationId"` + OrganizationRole *string `db:"organization_role" json:"organizationRole"` + AssociationLevel string `db:"association_level" json:"associationLevel"` }