From fcaeb14772ac32a22dc8fece2919592d31b9aa46 Mon Sep 17 00:00:00 2001 From: Oluwadara Date: Thu, 8 Aug 2024 15:20:59 +0100 Subject: [PATCH 1/4] feat: Add endpoint to delete organization --- src/controllers/OrgController.ts | 21 +++++++++++++++++++++ src/routes/organisation.ts | 9 +++++++++ src/services/org.services.ts | 23 ++++++++++++++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/controllers/OrgController.ts b/src/controllers/OrgController.ts index d635c044..6e894c41 100644 --- a/src/controllers/OrgController.ts +++ b/src/controllers/OrgController.ts @@ -184,6 +184,27 @@ export class OrgController { } } + async deleteOrganization(req: Request, res: Response, next: NextFunction) { + try { + const { org_id } = req.params; + const user = req.user; + const userId = user.id; + + const organizationService = new OrgService(); + await organizationService.deleteOrganization(org_id); + + const respObj = { + status: "success", + message: "Organization deleted successfully", + status_code: 200, + }; + + return res.status(200).json(respObj); + } catch (error) { + next(error); + } + } + /** * @swagger * /api/v1/users/{userId}/organizations: diff --git a/src/routes/organisation.ts b/src/routes/organisation.ts index 84a3aeee..ae895938 100644 --- a/src/routes/organisation.ts +++ b/src/routes/organisation.ts @@ -24,6 +24,15 @@ orgRouter.delete( validateOrgId, orgController.removeUser.bind(orgController), ); + +orgRouter.delete( + "/organizations/:org_id", + authMiddleware, + checkPermissions([UserRole.ADMIN, UserRole.ADMIN]), + validateOrgId, + orgController.deleteOrganization.bind(orgController), +); + orgRouter.get( "/organizations/:org_id/invite", authMiddleware, diff --git a/src/services/org.services.ts b/src/services/org.services.ts index cec628b1..88728c7c 100644 --- a/src/services/org.services.ts +++ b/src/services/org.services.ts @@ -4,7 +4,7 @@ import config from "../config/index"; import AppDataSource from "../data-source"; import { UserRole } from "../enums/userRoles"; import { BadRequest } from "../middleware"; -import { Conflict, ResourceNotFound } from "../middleware/error"; +import { Conflict, ResourceNotFound, ServerError } from "../middleware/error"; import { Invitation, OrgInviteToken, UserOrganization } from "../models"; import { Organization } from "../models/organization"; import { OrganizationRole } from "../models/organization-role.entity"; @@ -48,6 +48,27 @@ export class OrgService implements IOrgService { } } + public async deleteOrganization(orgId: string): Promise { + try { + const organization = await AppDataSource.manager.findOne(Organization, { + where: { id: orgId }, + }); + + if (!organization) { + throw new ResourceNotFound("Organization not found"); + } + + await AppDataSource.manager.remove(organization); + } catch (error) { + if (error instanceof ResourceNotFound) { + throw error; + } + throw new ServerError( + "An error occurred while deleting the organization", + ); + } + } + public async removeUser( org_id: string, user_id: string, From 245af9c9458e67c66d5dbd0ac0f3a8b1661a6196 Mon Sep 17 00:00:00 2001 From: Oluwadara Date: Thu, 8 Aug 2024 18:13:39 +0100 Subject: [PATCH 2/4] feat: Add endpoint to delete organization --- src/controllers/OrgController.ts | 74 +++++++++ .../1723129047097-CreateUsersTable.ts | 83 ++++++++++ src/services/org.services.ts | 30 +++- src/test/organisation.spec.ts | 151 ++++++++++++++++++ 4 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 src/migrations/1723129047097-CreateUsersTable.ts diff --git a/src/controllers/OrgController.ts b/src/controllers/OrgController.ts index 21490285..4406591f 100644 --- a/src/controllers/OrgController.ts +++ b/src/controllers/OrgController.ts @@ -186,6 +186,80 @@ export class OrgController { } } + /** + * @swagger + * /api/v1/organizations/{id}: + * delete: + * summary: Delete an organisation + * description: This endpoint allows an administrator to delete an organisation by its ID + * tags: [Organisation] + * operationId: deleteOrganisation + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * schema: + * type: string + * required: true + * description: The organisation ID + * responses: + * '200': + * description: Organisation deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: Organisation deleted successfully + * status_code: + * type: integer + * example: 200 + * '404': + * description: Organisation not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Organisation not found + * status_code: + * type: integer + * example: 404 + * '500': + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Internal server error + * status_code: + * type: integer + * example: 500 + * components: + * securitySchemes: + * bearerAuth: + * type: http + * scheme: bearer + * bearerFormat: JWT + */ + async deleteOrganization(req: Request, res: Response, next: NextFunction) { try { const { org_id } = req.params; diff --git a/src/migrations/1723129047097-CreateUsersTable.ts b/src/migrations/1723129047097-CreateUsersTable.ts new file mode 100644 index 00000000..fc4486d0 --- /dev/null +++ b/src/migrations/1723129047097-CreateUsersTable.ts @@ -0,0 +1,83 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateUsersTable1723129047097 implements MigrationInterface { + name = "CreateUsersTable1723129047097"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "comment" DROP CONSTRAINT "FK_5dec255234c5b7418f3d1e88ce4"`, + ); + await queryRunner.query( + `ALTER TABLE "invitation" DROP CONSTRAINT "FK_043292be3660aa8e5a46de7c4d7"`, + ); + await queryRunner.query( + `CREATE TABLE "squeeze" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "first_name" character varying, "last_name" character varying, "phone" character varying, "location" character varying, "job_title" character varying, "company" character varying, "interests" text, "referral_source" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_9ff9af91fd97e582b9ad37c99e5" UNIQUE ("email"), CONSTRAINT "PK_3a8521260c51931d22354bc9b41" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "news_letter_subscriber" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, CONSTRAINT "PK_f1b123b524ae7f4d46e3a435663" PRIMARY KEY ("id"))`, + ); + await queryRunner.query(`ALTER TABLE "comment" DROP COLUMN "blogId"`); + await queryRunner.query(`ALTER TABLE "contact" DROP COLUMN "phoneNumber"`); + await queryRunner.query( + `ALTER TABLE "invitation" DROP COLUMN "orgInviteTokenId"`, + ); + await queryRunner.query(`ALTER TABLE "comment" ADD "blog_id" uuid`); + await queryRunner.query(`ALTER TABLE "comment" ADD "user_id" uuid`); + await queryRunner.query( + `ALTER TABLE "invitation" ADD "isGeneric" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "invitation" ADD "isAccepted" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query(`ALTER TABLE "invitation" DROP COLUMN "token"`); + await queryRunner.query( + `ALTER TABLE "invitation" ADD "token" uuid NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "invitation" ALTER COLUMN "email" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "comment" ADD CONSTRAINT "FK_19494c2b9d227780622f8053d47" FOREIGN KEY ("blog_id") REFERENCES "blog"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "comment" ADD CONSTRAINT "FK_bbfe153fa60aa06483ed35ff4a7" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "comment" DROP CONSTRAINT "FK_bbfe153fa60aa06483ed35ff4a7"`, + ); + await queryRunner.query( + `ALTER TABLE "comment" DROP CONSTRAINT "FK_19494c2b9d227780622f8053d47"`, + ); + await queryRunner.query( + `ALTER TABLE "invitation" ALTER COLUMN "email" SET NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "invitation" DROP COLUMN "token"`); + await queryRunner.query( + `ALTER TABLE "invitation" ADD "token" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "invitation" DROP COLUMN "isAccepted"`, + ); + await queryRunner.query(`ALTER TABLE "invitation" DROP COLUMN "isGeneric"`); + await queryRunner.query(`ALTER TABLE "comment" DROP COLUMN "user_id"`); + await queryRunner.query(`ALTER TABLE "comment" DROP COLUMN "blog_id"`); + await queryRunner.query( + `ALTER TABLE "invitation" ADD "orgInviteTokenId" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "contact" ADD "phoneNumber" character varying(20) NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "comment" ADD "blogId" uuid`); + await queryRunner.query(`DROP TABLE "news_letter_subscriber"`); + await queryRunner.query(`DROP TABLE "squeeze"`); + await queryRunner.query( + `ALTER TABLE "invitation" ADD CONSTRAINT "FK_043292be3660aa8e5a46de7c4d7" FOREIGN KEY ("orgInviteTokenId") REFERENCES "org_invite_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "comment" ADD CONSTRAINT "FK_5dec255234c5b7418f3d1e88ce4" FOREIGN KEY ("blogId") REFERENCES "blog"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/src/services/org.services.ts b/src/services/org.services.ts index 8d127f04..1b8eae53 100644 --- a/src/services/org.services.ts +++ b/src/services/org.services.ts @@ -55,23 +55,47 @@ export class OrgService implements IOrgService { } public async deleteOrganization(orgId: string): Promise { + const queryRunner = AppDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { - const organization = await AppDataSource.manager.findOne(Organization, { + const organization = await queryRunner.manager.findOne(Organization, { where: { id: orgId }, + relations: [ + "userOrganizations", + "users", + "payments", + "billingPlans", + "products", + "role", + "organizationMembers", + ], }); if (!organization) { throw new ResourceNotFound("Organization not found"); } - - await AppDataSource.manager.remove(organization); + await queryRunner.manager.remove(organization.userOrganizations); + await queryRunner.manager.remove(organization.payments); + await queryRunner.manager.remove(organization.billingPlans); + await queryRunner.manager.remove(organization.products); + await queryRunner.manager.remove(organization.role); + await queryRunner.manager.remove(organization.organizationMembers); + + await queryRunner.manager.remove(organization); + await queryRunner.commitTransaction(); } catch (error) { + await queryRunner.rollbackTransaction(); if (error instanceof ResourceNotFound) { throw error; } throw new ServerError( "An error occurred while deleting the organization", ); + } finally { + await queryRunner.release(); } } diff --git a/src/test/organisation.spec.ts b/src/test/organisation.spec.ts index 2a95145f..9bbb98fc 100644 --- a/src/test/organisation.spec.ts +++ b/src/test/organisation.spec.ts @@ -352,3 +352,154 @@ describe("Update User Organization", () => { expect(mockRepository.update).not.toHaveBeenCalled(); }); }); + +//---------------- +// New tests for deleteOrganization + +describe("deleteOrganization", () => { + let orgService: OrgService; + let mockQueryRunner; + let organizationRepositoryMock: jest.Mocked>; + + beforeEach(() => { + mockQueryRunner = { + connect: jest.fn(), + startTransaction: jest.fn(), + commitTransaction: jest.fn(), + rollbackTransaction: jest.fn(), + release: jest.fn(), + manager: { + findOne: jest.fn(), + remove: jest.fn(), + }, + }; + + AppDataSource.createQueryRunner = jest + .fn() + .mockReturnValue(mockQueryRunner); + + organizationRepositoryMock = { + findOne: jest.fn(), + } as any; + + (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { + if (entity === Organization) return organizationRepositoryMock; + return {}; + }); + + orgService = new OrgService(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should delete an organization successfully", async () => { + const orgId = "123e4567-e89b-12d3-a456-426614174000"; + const mockOrganization = { + id: orgId, + userOrganizations: [], + users: [], + payments: [], + billingPlans: [], + products: [], + role: {}, + organizationMembers: [], + }; + + mockQueryRunner.manager.findOne.mockResolvedValue(mockOrganization); + + await orgService.deleteOrganization(orgId); + + expect(mockQueryRunner.connect).toHaveBeenCalled(); + expect(mockQueryRunner.startTransaction).toHaveBeenCalled(); + expect(mockQueryRunner.manager.findOne).toHaveBeenCalledWith(Organization, { + where: { id: orgId }, + relations: [ + "userOrganizations", + "users", + "payments", + "billingPlans", + "products", + "role", + "organizationMembers", + ], + }); + expect(mockQueryRunner.manager.remove).toHaveBeenCalledTimes(7); // Updated expected call count to 7 + expect(mockQueryRunner.manager.remove).toHaveBeenCalledWith( + mockOrganization.userOrganizations, + ); + expect(mockQueryRunner.manager.remove).toHaveBeenCalledWith( + mockOrganization.payments, + ); + expect(mockQueryRunner.manager.remove).toHaveBeenCalledWith( + mockOrganization.billingPlans, + ); + expect(mockQueryRunner.manager.remove).toHaveBeenCalledWith( + mockOrganization.products, + ); + expect(mockQueryRunner.manager.remove).toHaveBeenCalledWith( + mockOrganization.role, + ); + expect(mockQueryRunner.manager.remove).toHaveBeenCalledWith( + mockOrganization.organizationMembers, + ); + expect(mockQueryRunner.manager.remove).toHaveBeenCalledWith( + mockOrganization, + ); + expect(mockQueryRunner.commitTransaction).toHaveBeenCalled(); + }); + + it("should throw ResourceNotFound if organization does not exist", async () => { + const orgId = "123e4567-e89b-12d3-a456-426614174000"; + + mockQueryRunner.manager.findOne.mockResolvedValue(null); + + await expect(orgService.deleteOrganization(orgId)).rejects.toThrow( + ResourceNotFound, + ); + + expect(mockQueryRunner.connect).toHaveBeenCalled(); + expect(mockQueryRunner.startTransaction).toHaveBeenCalled(); + expect(mockQueryRunner.manager.findOne).toHaveBeenCalledWith(Organization, { + where: { id: orgId }, + relations: [ + "userOrganizations", + "users", + "payments", + "billingPlans", + "products", + "role", + "organizationMembers", + ], + }); + expect(mockQueryRunner.rollbackTransaction).toHaveBeenCalled(); + }); + + it("should throw ServerError on transaction failure", async () => { + const orgId = "123e4567-e89b-12d3-a456-426614174000"; + const mockError = new Error("Test Error"); + + mockQueryRunner.manager.findOne.mockRejectedValue(mockError); + + await expect(orgService.deleteOrganization(orgId)).rejects.toThrow( + ServerError, + ); + + expect(mockQueryRunner.connect).toHaveBeenCalled(); + expect(mockQueryRunner.startTransaction).toHaveBeenCalled(); + expect(mockQueryRunner.manager.findOne).toHaveBeenCalledWith(Organization, { + where: { id: orgId }, + relations: [ + "userOrganizations", + "users", + "payments", + "billingPlans", + "products", + "role", + "organizationMembers", + ], + }); + expect(mockQueryRunner.rollbackTransaction).toHaveBeenCalled(); + }); +}); From a007f3919f547eea71d9cbb3f88c5450bb2ab2d2 Mon Sep 17 00:00:00 2001 From: Robinson Uchechukwu Date: Thu, 8 Aug 2024 18:41:11 +0100 Subject: [PATCH 3/4] use ubuntu latest runner dev.yml --- .github/workflows/dev.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index a2d32531..5e67ddce 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -8,9 +8,12 @@ on: branches: [dev] jobs: - on-success: + build: runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion=='success' && github.event.workflow_run.head_branch == 'dev' } + defaults: + run: + working-directory: /var/www/aihomework/dev + steps: - name: Checkout code uses: actions/checkout@v3 From d07812fb8e892437eb4c812372cbab1f5883712e Mon Sep 17 00:00:00 2001 From: Robinson Uchechukwu Date: Thu, 8 Aug 2024 18:57:06 +0100 Subject: [PATCH 4/4] Update runner dev.yml --- .github/workflows/dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 5e67ddce..676cf9b9 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: self-hosted defaults: run: working-directory: /var/www/aihomework/dev