From 1c5095eb9396d66956e2963af3251254407f9b79 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 24 Aug 2025 23:39:57 +0200 Subject: [PATCH] add tests --- package-lock.json | 4 +- src/guards/token.guard.spec.ts | 196 +++++++++++++++++ src/services/group.factory.service.spec.ts | 112 ++++++++++ src/services/group.service.spec.ts | 193 ++++++++++++++++ src/services/login.service.spec.ts | 150 +++++++++++++ .../organization.factory.service.spec.ts | 49 +++++ src/services/organization.service.spec.ts | 130 +++++++++++ src/services/registration.service.spec.ts | 207 ++++++++++++++++++ src/services/right.factory.service.spec.ts | 70 ++++++ src/services/right.service.spec.ts | 113 ++++++++++ 10 files changed, 1222 insertions(+), 2 deletions(-) create mode 100644 src/guards/token.guard.spec.ts create mode 100644 src/services/group.factory.service.spec.ts create mode 100644 src/services/group.service.spec.ts create mode 100644 src/services/login.service.spec.ts create mode 100644 src/services/organization.factory.service.spec.ts create mode 100644 src/services/organization.service.spec.ts create mode 100644 src/services/registration.service.spec.ts create mode 100644 src/services/right.factory.service.spec.ts create mode 100644 src/services/right.service.spec.ts diff --git a/package-lock.json b/package-lock.json index b15ef4f..03dc7bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apihub24/token-authentication", - "version": "1.0.6", + "version": "2.0.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apihub24/token-authentication", - "version": "1.0.6", + "version": "2.0.0-alpha.0", "license": "MIT", "dependencies": { "@apihub24/authentication": "2.0.0-alpha.0", diff --git a/src/guards/token.guard.spec.ts b/src/guards/token.guard.spec.ts new file mode 100644 index 0000000..1370c42 --- /dev/null +++ b/src/guards/token.guard.spec.ts @@ -0,0 +1,196 @@ +import { ExecutionContext } from "@nestjs/common"; +import { TokenGuard } from "./token.guard"; +import { Reflector } from "@nestjs/core"; +import { + IAccount, + IGroup, + IOrganization, + IRight, + ITokenService, +} from "@apihub24/authentication"; +import { RIGHTS_KEY } from "./rights.decorator"; +import { ROLES_KEY } from "./roles.decorator"; +import { ORGANIZATIONS_KEY } from "./organization.decorator"; + +describe("TokenGuard Tests", () => { + let guard: TokenGuard; + + const mockRight: IRight = { id: "r1", name: "read:users" }; + const mockOrganization: IOrganization = { id: "o1", name: "TestOrg" }; + const mockGroup: IGroup = { + id: "g1", + name: "admin", + rights: [mockRight], + organization: mockOrganization, + }; + const mockAccount: IAccount = { + id: "a1", + accountName: "a1", + email: "a1@example.de", + passwordHash: "passwordhash", + active: true, + emailVerified: true, + groups: [mockGroup], + }; + const mockReflector = { + getAllAndOverride: jest.fn(), + }; + const mockTokenService = { + getAccount: jest.fn(), + }; + + const mockExecutionContext = { + getHandler: () => ({}), + getClass: () => ({}), + switchToHttp: () => ({ + getRequest: () => ({ + headers: {}, + }), + }), + } as unknown as ExecutionContext; + + beforeAll(async () => { + jest.clearAllMocks(); + guard = new TokenGuard( + mockReflector as unknown as Reflector, + mockTokenService as unknown as ITokenService + ); + }); + + it("should be defined", () => { + expect(guard).toBeDefined(); + }); + + it("should return true if no metadata is required", async () => { + mockReflector.getAllAndOverride.mockReturnValue(undefined); + const result = await guard.canActivate(mockExecutionContext); + expect(result).toBe(true); + expect(mockTokenService.getAccount).not.toHaveBeenCalled(); + }); + + describe("Token Validation", () => { + it("should return false if authorization header is missing", async () => { + mockReflector.getAllAndOverride.mockReturnValue(["read:users"]); + const context = { + getHandler: () => ({}), + getClass: () => ({}), + switchToHttp: () => ({ getRequest: () => ({ headers: {} }) }), + } as unknown as ExecutionContext; + + const result = await guard.canActivate(context); + expect(result).toBe(false); + expect(mockTokenService.getAccount).not.toHaveBeenCalled(); + }); + + it("should return false if token is invalid", async () => { + mockReflector.getAllAndOverride.mockReturnValue(["read:users"]); + mockTokenService.getAccount.mockResolvedValue(null); + const result = await guard.canActivate(mockExecutionContext); + expect(result).toBe(false); + }); + }); + + describe("Successful Authorization", () => { + it("should return true if account has required right", async () => { + mockReflector.getAllAndOverride.mockImplementation((key: string) => { + if (key === RIGHTS_KEY) return ["read:users"]; + return undefined; + }); + mockTokenService.getAccount.mockResolvedValue(mockAccount); + const context = { + getHandler: () => ({}), + getClass: () => ({}), + switchToHttp: () => ({ + getRequest: () => ({ headers: { authorization: "Bearer bla" } }), + }), + } as unknown as ExecutionContext; + const result = await guard.canActivate(context); + expect(result).toBe(true); + }); + + it("should return true if account has required role", async () => { + mockReflector.getAllAndOverride.mockImplementation((key: string) => { + if (key === ROLES_KEY) return ["admin"]; + return undefined; + }); + mockTokenService.getAccount.mockResolvedValue(mockAccount); + const context = { + getHandler: () => ({}), + getClass: () => ({}), + switchToHttp: () => ({ + getRequest: () => ({ headers: { authorization: "Bearer bla" } }), + }), + } as unknown as ExecutionContext; + const result = await guard.canActivate(context); + expect(result).toBe(true); + }); + + it("should return true if account belongs to the required organization", async () => { + mockReflector.getAllAndOverride.mockImplementation((key: string) => { + if (key === ORGANIZATIONS_KEY) return ["TestOrg"]; + return undefined; + }); + mockTokenService.getAccount.mockResolvedValue(mockAccount); + const context = { + getHandler: () => ({}), + getClass: () => ({}), + switchToHttp: () => ({ + getRequest: () => ({ headers: { authorization: "Bearer bla" } }), + }), + } as unknown as ExecutionContext; + const result = await guard.canActivate(context); + expect(result).toBe(true); + }); + + it("should return true if all requirements are met", async () => { + mockReflector.getAllAndOverride.mockImplementation((key: string) => { + if (key === RIGHTS_KEY) return ["read:users"]; + if (key === ROLES_KEY) return ["admin"]; + if (key === ORGANIZATIONS_KEY) return ["TestOrg"]; + return undefined; + }); + mockTokenService.getAccount.mockResolvedValue(mockAccount); + const context = { + getHandler: () => ({}), + getClass: () => ({}), + switchToHttp: () => ({ + getRequest: () => ({ headers: { authorization: "Bearer bla" } }), + }), + } as unknown as ExecutionContext; + const result = await guard.canActivate(context); + expect(result).toBe(true); + }); + }); + + describe("Failed Authorization", () => { + it("should return false if a required right is missing", async () => { + mockReflector.getAllAndOverride.mockImplementation((key: string) => { + if (key === RIGHTS_KEY) return ["write:users"]; + return undefined; + }); + mockTokenService.getAccount.mockResolvedValue(mockAccount); + const result = await guard.canActivate(mockExecutionContext); + expect(result).toBe(false); + }); + + it("should return false if a required role is missing", async () => { + mockReflector.getAllAndOverride.mockImplementation((key: string) => { + if (key === ROLES_KEY) return ["user"]; + return undefined; + }); + mockTokenService.getAccount.mockResolvedValue(mockAccount); + const result = await guard.canActivate(mockExecutionContext); + expect(result).toBe(false); + }); + + it("should return false if a required organization is missing", async () => { + mockReflector.getAllAndOverride.mockImplementation((key: string) => { + if (key === ORGANIZATIONS_KEY) return ["AnotherOrg"]; + return undefined; + }); + mockTokenService.getAccount.mockResolvedValue(mockAccount); + const result = await guard.canActivate(mockExecutionContext); + expect(result).toBe(false); + }); + }); +}); diff --git a/src/services/group.factory.service.spec.ts b/src/services/group.factory.service.spec.ts new file mode 100644 index 0000000..b53884b --- /dev/null +++ b/src/services/group.factory.service.spec.ts @@ -0,0 +1,112 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { MailServiceMockModule } from "../../test/mail.verification.service.mock"; +import { PasswordServiceMockModule } from "../../test/password.service.mock"; +import { SessionServiceMockModule } from "../../test/session.service.mock"; +import { TokenServiceMockModule } from "../../test/token.service.mock"; +import { AccountRepositoryMockModule } from "../../test/account.repository.mock"; +import { GroupRepositoryMockModule } from "../../test/group.repository.mock"; +import { RightRepositoryMockModule } from "../../test/right.repository.mock"; +import { OrganizationRepositoryMockModule } from "../../test/organization.repository.mock"; +import { + GroupFactoryService, + OrganizationService, + RightService, + TokenAuthenticationModule, +} from ".."; +import { IOrganization, IRight } from "@apihub24/authentication"; +import * as validator from "class-validator"; +import { Group } from "../models/group"; + +describe("GroupFactoryService Tests", () => { + let module: TestingModule; + let service: GroupFactoryService; + let rightService: RightService; + let organizationService: OrganizationService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MailServiceMockModule.forRoot(), + PasswordServiceMockModule.forRoot(), + SessionServiceMockModule.forRoot(), + TokenServiceMockModule.forRoot(), + AccountRepositoryMockModule.forRoot(), + GroupRepositoryMockModule.forRoot(), + RightRepositoryMockModule.forRoot(), + OrganizationRepositoryMockModule.forRoot(), + TokenAuthenticationModule.forRoot(), + ], + }).compile(); + + service = module.get(GroupFactoryService); + rightService = module.get(RightService); + organizationService = module.get(OrganizationService); + + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + it("should create a group successfully", async () => { + const rights: IRight[] = [ + { id: "1", name: "read" }, + { id: "2", name: "write" }, + ]; + const organizations: IOrganization[] = [{ id: "org-1", name: "Test Org" }]; + const groupName = "Admins"; + const organizationId = "org-1"; + const rightIds = ["1", "2"]; + + jest.spyOn(rightService, "getBy").mockResolvedValue(rights); + jest.spyOn(organizationService, "getBy").mockResolvedValue(organizations); + jest.spyOn(validator, "validate").mockResolvedValue([]); + + const result = await service.createGroup( + groupName, + organizationId, + rightIds + ); + + expect(rightService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(organizationService.getBy).toHaveBeenCalledWith( + expect.any(Function) + ); + expect(result).toBeInstanceOf(Group); + expect(result.name).toBe(groupName); + expect(result.organization).toBe(organizations[0]); + expect(result.rights).toBe(rights); + }); + + it("should throw an error if the organization does not exist", async () => { + const groupName = "Admins"; + const organizationId = "org-1"; + const rightIds = ["1", "2"]; + + jest.spyOn(rightService, "getBy").mockResolvedValue([]); + jest.spyOn(organizationService, "getBy").mockResolvedValue([]); + jest.spyOn(validator, "validate").mockResolvedValue([]); + + await expect( + service.createGroup(groupName, organizationId, rightIds) + ).rejects.toThrow(`organization with id (${organizationId}) not exists`); + }); + + it("should throw a validation error if the group data is invalid", async () => { + const organizations: IOrganization[] = [{ id: "org-1", name: "Test Org" }]; + const validationError: validator.ValidationError[] = [ + { toString: () => "name must not be empty" } as validator.ValidationError, + ]; + + jest.spyOn(rightService, "getBy").mockResolvedValue([]); + jest.spyOn(organizationService, "getBy").mockResolvedValue(organizations); + jest + .spyOn(validator, "validate") + .mockResolvedValue(Promise.resolve(validationError)); + + await expect(service.createGroup("Admins", "org-1", ["1"])).rejects.toThrow( + "name must not be empty" + ); + }); +}); diff --git a/src/services/group.service.spec.ts b/src/services/group.service.spec.ts new file mode 100644 index 0000000..5302fca --- /dev/null +++ b/src/services/group.service.spec.ts @@ -0,0 +1,193 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { GroupService } from "./group.service"; +import { IRepository } from "@apihub24/repository"; +import { + APIHUB24_GROUP_REPOSITORY, + APIHUB24_RIGHT_REPOSITORY, + IGroup, + IRight, +} from "@apihub24/authentication"; +import { MailServiceMockModule } from "../../test/mail.verification.service.mock"; +import { PasswordServiceMockModule } from "../../test/password.service.mock"; +import { SessionServiceMockModule } from "../../test/session.service.mock"; +import { TokenServiceMockModule } from "../../test/token.service.mock"; +import { AccountRepositoryMockModule } from "../../test/account.repository.mock"; +import { GroupRepositoryMockModule } from "../../test/group.repository.mock"; +import { RightRepositoryMockModule } from "../../test/right.repository.mock"; +import { OrganizationRepositoryMockModule } from "../../test/organization.repository.mock"; +import { TokenAuthenticationModule } from ".."; + +describe("GroupService Tests", () => { + let module: TestingModule; + let service: GroupService; + let groupRepository: IRepository; + let rightRepository: IRepository; + + const mockGroup: IGroup = { + id: "group1", + name: "Admins", + rights: [{ id: "right1", name: "read" }], + organization: { id: "org1", name: "Test Org" }, + }; + + const mockRight: IRight = { + id: "right2", + name: "write", + }; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MailServiceMockModule.forRoot(), + PasswordServiceMockModule.forRoot(), + SessionServiceMockModule.forRoot(), + TokenServiceMockModule.forRoot(), + AccountRepositoryMockModule.forRoot(), + GroupRepositoryMockModule.forRoot(), + RightRepositoryMockModule.forRoot(), + OrganizationRepositoryMockModule.forRoot(), + TokenAuthenticationModule.forRoot(), + ], + }).compile(); + + service = module.get(GroupService); + groupRepository = module.get(APIHUB24_GROUP_REPOSITORY); + rightRepository = module.get(APIHUB24_RIGHT_REPOSITORY); + + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("getBy", () => { + it("should return groups matching the filter", async () => { + jest.spyOn(groupRepository, "getBy").mockResolvedValue([mockGroup]); + const result = await service.getBy((g) => g.id === "group1"); + expect(result).toEqual([mockGroup]); + expect(groupRepository.getBy).toHaveBeenCalledWith(expect.any(Function)); + }); + + it("should return an empty array if no groups match", async () => { + jest.spyOn(groupRepository, "getBy").mockResolvedValue([]); + const result = await service.getBy((g) => g.id === "non-existent"); + expect(result).toEqual([]); + }); + }); + + describe("save", () => { + it("should save a single group and return it", async () => { + jest.spyOn(groupRepository, "save").mockResolvedValue([mockGroup]); + const result = await service.save(mockGroup); + expect(result).toEqual(mockGroup); + expect(groupRepository.save).toHaveBeenCalledWith([mockGroup]); + }); + + it("should return null if save operation fails", async () => { + jest.spyOn(groupRepository, "save").mockResolvedValue([]); + const result = await service.save(mockGroup); + expect(result).toBeNull(); + }); + }); + + describe("delete", () => { + it("should delete groups and return true on success", async () => { + jest.spyOn(groupRepository, "deleteBy").mockResolvedValue(true); + const result = await service.delete((g) => g.id === "group1"); + expect(result).toBe(true); + expect(groupRepository.deleteBy).toHaveBeenCalledWith( + expect.any(Function) + ); + }); + + it("should return false on deletion failure", async () => { + jest.spyOn(groupRepository, "deleteBy").mockResolvedValue(false); + const result = await service.delete((g) => g.id === "non-existent"); + expect(result).toBe(false); + }); + }); + + describe("addRightToGroup", () => { + it("should add a right to a group and return the updated group", async () => { + const updatedGroup = { + ...mockGroup, + rights: [...mockGroup.rights, mockRight], + }; + jest.spyOn(groupRepository, "getBy").mockResolvedValue([mockGroup]); + jest.spyOn(rightRepository, "getBy").mockResolvedValue([mockRight]); + jest.spyOn(groupRepository, "save").mockResolvedValue([updatedGroup]); + + const result = await service.addRightToGroup("group1", "right2"); + + expect(result).toEqual(updatedGroup); + expect(groupRepository.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(rightRepository.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(groupRepository.save).toHaveBeenCalledWith([ + expect.objectContaining({ + rights: expect.arrayContaining([mockRight]), + }), + ]); + }); + + it("should throw an error if the group is not found", async () => { + jest.spyOn(groupRepository, "getBy").mockResolvedValue([]); + jest.spyOn(rightRepository, "getBy").mockResolvedValue([mockRight]); + await expect( + service.addRightToGroup("non-existent", "right2") + ).rejects.toThrow("group with id non-existent not found"); + }); + + it("should throw an error if the right is not found", async () => { + jest.spyOn(groupRepository, "getBy").mockResolvedValue([mockGroup]); + jest.spyOn(rightRepository, "getBy").mockResolvedValue([]); + await expect( + service.addRightToGroup("group1", "non-existent") + ).rejects.toThrow("right with id non-existent not found"); + }); + + it("should throw an error if the updated group cannot be saved", async () => { + jest.spyOn(groupRepository, "getBy").mockResolvedValue([mockGroup]); + jest.spyOn(rightRepository, "getBy").mockResolvedValue([mockRight]); + jest.spyOn(groupRepository, "save").mockResolvedValue([]); + await expect(service.addRightToGroup("group1", "right2")).rejects.toThrow( + "group Admins can not be saved" + ); + }); + }); + + describe("removeRightFromGroup", () => { + it("should remove a right from a group and return the updated group", async () => { + const groupWithMultipleRights: IGroup = { + ...mockGroup, + rights: [...mockGroup.rights, mockRight], + }; + const updatedGroup = { ...groupWithMultipleRights, rights: [mockRight] }; + jest + .spyOn(groupRepository, "getBy") + .mockResolvedValue([groupWithMultipleRights]); + jest.spyOn(rightRepository, "getBy").mockResolvedValue([mockRight]); + jest.spyOn(groupRepository, "save").mockResolvedValue([updatedGroup]); + + const result = await service.removeRightFromGroup("group1", "right1"); + + expect(result).toEqual(updatedGroup); + expect(groupRepository.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(rightRepository.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(groupRepository.save).toHaveBeenCalledWith([ + expect.objectContaining({ + rights: expect.not.arrayContaining([mockGroup.rights[0]]), + }), + ]); + }); + + it("should throw an error if the group cannot be saved", async () => { + jest.spyOn(groupRepository, "getBy").mockResolvedValue([mockGroup]); + jest.spyOn(rightRepository, "getBy").mockResolvedValue([mockRight]); + jest.spyOn(groupRepository, "save").mockResolvedValue([]); + await expect( + service.removeRightFromGroup("group1", "right1") + ).rejects.toThrow("group Admins can not be saved"); + }); + }); +}); diff --git a/src/services/login.service.spec.ts b/src/services/login.service.spec.ts new file mode 100644 index 0000000..1c4d3e5 --- /dev/null +++ b/src/services/login.service.spec.ts @@ -0,0 +1,150 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { LoginService } from "./login.service"; +import { MailServiceMockModule } from "../../test/mail.verification.service.mock"; +import { PasswordServiceMockModule } from "../../test/password.service.mock"; +import { SessionServiceMockModule } from "../../test/session.service.mock"; +import { TokenServiceMockModule } from "../../test/token.service.mock"; +import { AccountRepositoryMockModule } from "../../test/account.repository.mock"; +import { GroupRepositoryMockModule } from "../../test/group.repository.mock"; +import { RightRepositoryMockModule } from "../../test/right.repository.mock"; +import { OrganizationRepositoryMockModule } from "../../test/organization.repository.mock"; +import { AccountService, TokenAuthenticationModule } from ".."; +import { Account } from "../models/account"; +import { Session } from "../models/session"; +import { SignIn } from "../models/sign.in"; +import { + APIHUB24_PASSWORD_SERVICE, + APIHUB24_SESSION_SERVICE, + APIHUB24_TOKEN_SERVICE, + IPasswordService, + ISessionService, + ITokenService, +} from "@apihub24/authentication"; + +describe("LoginService Test", () => { + let module: TestingModule; + let service: LoginService; + let accountService: AccountService; + let passwordService: IPasswordService; + let sessionService: ISessionService; + let tokenService: ITokenService; + + const mockAccount: Account = { + id: "account1", + accountName: "testuser", + passwordHash: "hashedpassword", + email: "testuser@example.de", + emailVerified: true, + active: true, + groups: [], + }; + const mockSession: Session = { + id: "session1", + account: mockAccount, + metaData: {}, + }; + const mockSignIn: SignIn = { + accountName: "testuser", + password: "password123", + }; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MailServiceMockModule.forRoot(), + PasswordServiceMockModule.forRoot(), + SessionServiceMockModule.forRoot(), + TokenServiceMockModule.forRoot(), + AccountRepositoryMockModule.forRoot(), + GroupRepositoryMockModule.forRoot(), + RightRepositoryMockModule.forRoot(), + OrganizationRepositoryMockModule.forRoot(), + TokenAuthenticationModule.forRoot(), + ], + }).compile(); + + service = module.get(LoginService); + accountService = module.get(AccountService); + passwordService = module.get(APIHUB24_PASSWORD_SERVICE); + sessionService = module.get(APIHUB24_SESSION_SERVICE); + tokenService = module.get(APIHUB24_TOKEN_SERVICE); + + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("signIn", () => { + it("should return a token for a valid account", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([mockAccount]); + jest.spyOn(passwordService, "verify").mockResolvedValue(true); + jest.spyOn(sessionService, "create").mockResolvedValue(mockSession); + jest.spyOn(tokenService, "generate").mockResolvedValue("jwt_token"); + + const result = await service.signIn(mockSignIn); + + expect(accountService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(passwordService.verify).toHaveBeenCalledWith( + mockSignIn.password, + mockAccount.passwordHash + ); + expect(sessionService.create).toHaveBeenCalledWith(mockAccount); + expect(tokenService.generate).toHaveBeenCalledWith( + mockSession, + mockSignIn.accountName, + "1h", + "HS512" + ); + expect(result).toBe("jwt_token"); + }); + + it("should return an empty string if account is not found", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([]); + const result = await service.signIn(mockSignIn); + expect(result).toBe(""); + }); + + it("should return an empty string if password verification fails", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([mockAccount]); + jest.spyOn(passwordService, "verify").mockResolvedValue(false); + const result = await service.signIn(mockSignIn); + expect(result).toBe(""); + }); + + it("should return an empty string if the account has no password hash", async () => { + const noPasswordHashAccount = { ...mockAccount, passwordHash: "" }; + jest + .spyOn(accountService, "getBy") + .mockResolvedValue([noPasswordHashAccount]); + const result = await service.signIn(mockSignIn); + expect(result).toBe(""); + }); + }); + + describe("signOut", () => { + it("should successfully remove a session", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([mockAccount]); + jest.spyOn(sessionService, "getBy").mockResolvedValue([mockSession]); + jest.spyOn(sessionService, "remove").mockResolvedValue(); + + await service.signOut("account1"); + + expect(accountService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(sessionService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(sessionService.remove).toHaveBeenCalledWith(mockSession.id); + }); + + it("should do nothing if the account is not found", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([]); + await service.signOut("non-existent-id"); + }); + + it("should do nothing if no session is found for the account", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([mockAccount]); + jest.spyOn(sessionService, "getBy").mockResolvedValue([]); + await service.signOut("account1"); + }); + }); +}); diff --git a/src/services/organization.factory.service.spec.ts b/src/services/organization.factory.service.spec.ts new file mode 100644 index 0000000..f5336ea --- /dev/null +++ b/src/services/organization.factory.service.spec.ts @@ -0,0 +1,49 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { OrganizationFactoryService } from "./organization.factory.service"; +import { MailServiceMockModule } from "../../test/mail.verification.service.mock"; +import { PasswordServiceMockModule } from "../../test/password.service.mock"; +import { SessionServiceMockModule } from "../../test/session.service.mock"; +import { TokenServiceMockModule } from "../../test/token.service.mock"; +import { AccountRepositoryMockModule } from "../../test/account.repository.mock"; +import { GroupRepositoryMockModule } from "../../test/group.repository.mock"; +import { RightRepositoryMockModule } from "../../test/right.repository.mock"; +import { OrganizationRepositoryMockModule } from "../../test/organization.repository.mock"; +import { TokenAuthenticationModule } from ".."; +import { Organization } from "../models/organization"; + +describe("OrganizationFactoryService Tests", () => { + let module: TestingModule; + let service: OrganizationFactoryService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MailServiceMockModule.forRoot(), + PasswordServiceMockModule.forRoot(), + SessionServiceMockModule.forRoot(), + TokenServiceMockModule.forRoot(), + AccountRepositoryMockModule.forRoot(), + GroupRepositoryMockModule.forRoot(), + RightRepositoryMockModule.forRoot(), + OrganizationRepositoryMockModule.forRoot(), + TokenAuthenticationModule.forRoot(), + ], + }).compile(); + + service = module.get(OrganizationFactoryService); + + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + it("should create an organization with the correct name", () => { + const orgName = "Test Organization"; + const result = service.createFromName(orgName); + + expect(result).toBeInstanceOf(Organization); + expect(result.name).toBe(orgName); + }); +}); diff --git a/src/services/organization.service.spec.ts b/src/services/organization.service.spec.ts new file mode 100644 index 0000000..de9f95a --- /dev/null +++ b/src/services/organization.service.spec.ts @@ -0,0 +1,130 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { OrganizationService } from "./organization.service"; +import { IRepository } from "@apihub24/repository"; +import { + APIHUB24_ORGANIZATION_REPOSITORY, + IOrganization, +} from "@apihub24/authentication"; +import { MailServiceMockModule } from "../../test/mail.verification.service.mock"; +import { PasswordServiceMockModule } from "../../test/password.service.mock"; +import { SessionServiceMockModule } from "../../test/session.service.mock"; +import { TokenServiceMockModule } from "../../test/token.service.mock"; +import { AccountRepositoryMockModule } from "../../test/account.repository.mock"; +import { GroupRepositoryMockModule } from "../../test/group.repository.mock"; +import { RightRepositoryMockModule } from "../../test/right.repository.mock"; +import { OrganizationRepositoryMockModule } from "../../test/organization.repository.mock"; +import { TokenAuthenticationModule } from "../token.authentication.module"; + +describe("OrganizationService Tests", () => { + let module: TestingModule; + let service: OrganizationService; + let organizationRepository: IRepository; + + const mockOrganization: IOrganization = { + id: "org1", + name: "Test Organization", + }; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MailServiceMockModule.forRoot(), + PasswordServiceMockModule.forRoot(), + SessionServiceMockModule.forRoot(), + TokenServiceMockModule.forRoot(), + AccountRepositoryMockModule.forRoot(), + GroupRepositoryMockModule.forRoot(), + RightRepositoryMockModule.forRoot(), + OrganizationRepositoryMockModule.forRoot(), + TokenAuthenticationModule.forRoot(), + ], + }).compile(); + + service = module.get(OrganizationService); + organizationRepository = module.get(APIHUB24_ORGANIZATION_REPOSITORY); + + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("getBy", () => { + it("should return organizations matching the filter", async () => { + jest + .spyOn(organizationRepository, "getBy") + .mockResolvedValue([mockOrganization]); + + const result = await service.getBy((org) => org.id === "org1"); + + expect(result).toEqual([mockOrganization]); + expect(organizationRepository.getBy).toHaveBeenCalledWith( + expect.any(Function) + ); + }); + + it("should return an empty array if no organizations match", async () => { + jest.spyOn(organizationRepository, "getBy").mockResolvedValue([]); + + const result = await service.getBy((org) => org.id === "non-existent-id"); + + expect(result).toEqual([]); + expect(organizationRepository.getBy).toHaveBeenCalledWith( + expect.any(Function) + ); + }); + }); + + describe("save", () => { + it("should save a single organization and return it", async () => { + jest + .spyOn(organizationRepository, "save") + .mockResolvedValue([mockOrganization]); + + const result = await service.save(mockOrganization); + + expect(result).toEqual(mockOrganization); + expect(organizationRepository.save).toHaveBeenCalledWith([ + mockOrganization, + ]); + }); + + it("should throw an error if the save operation fails", async () => { + jest.spyOn(organizationRepository, "save").mockResolvedValue([]); + + await expect(service.save(mockOrganization)).rejects.toThrow( + `organization (${mockOrganization.name}) not saved` + ); + expect(organizationRepository.save).toHaveBeenCalledWith([ + mockOrganization, + ]); + }); + }); + + describe("deleteBy", () => { + it("should delete organizations and return true on success", async () => { + jest.spyOn(organizationRepository, "deleteBy").mockResolvedValue(true); + + const result = await service.deleteBy((org) => org.id === "org1"); + + expect(result).toBe(true); + expect(organizationRepository.deleteBy).toHaveBeenCalledWith( + expect.any(Function) + ); + }); + + it("should return false on deletion failure", async () => { + jest.spyOn(organizationRepository, "deleteBy").mockResolvedValue(false); + + const result = await service.deleteBy( + (org) => org.id === "non-existent-id" + ); + + expect(result).toBe(false); + expect(organizationRepository.deleteBy).toHaveBeenCalledWith( + expect.any(Function) + ); + }); + }); +}); diff --git a/src/services/registration.service.spec.ts b/src/services/registration.service.spec.ts new file mode 100644 index 0000000..340c3f8 --- /dev/null +++ b/src/services/registration.service.spec.ts @@ -0,0 +1,207 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { RegistrationService } from "./registration.service"; +import { MailServiceMockModule } from "../../test/mail.verification.service.mock"; +import { PasswordServiceMockModule } from "../../test/password.service.mock"; +import { SessionServiceMockModule } from "../../test/session.service.mock"; +import { TokenServiceMockModule } from "../../test/token.service.mock"; +import { AccountRepositoryMockModule } from "../../test/account.repository.mock"; +import { GroupRepositoryMockModule } from "../../test/group.repository.mock"; +import { RightRepositoryMockModule } from "../../test/right.repository.mock"; +import { OrganizationRepositoryMockModule } from "../../test/organization.repository.mock"; +import { + AccountFactoryService, + AccountService, + TokenAuthenticationModule, +} from ".."; +import { Account } from "../models/account"; +import { + APIHUB24_MAIL_SERVICE, + IMailVerificationService, + IRegistration, +} from "@apihub24/authentication"; + +describe("RegistrationService Tests", () => { + let module: TestingModule; + let service: RegistrationService; + let accountFactoryService: AccountFactoryService; + let accountService: AccountService; + let mailVerificationService: IMailVerificationService; + + const mockRegistration: IRegistration = { + accountName: "testuser", + email: "test@example.com", + password: "password123", + passwordComparer: "password123", + groupIds: [], + }; + const mockAccount: Account = { + id: "account1", + accountName: "testuser", + email: "test@example.com", + emailVerified: false, + active: false, + passwordHash: "passwordhash", + groups: [], + }; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MailServiceMockModule.forRoot(), + PasswordServiceMockModule.forRoot(), + SessionServiceMockModule.forRoot(), + TokenServiceMockModule.forRoot(), + AccountRepositoryMockModule.forRoot(), + GroupRepositoryMockModule.forRoot(), + RightRepositoryMockModule.forRoot(), + OrganizationRepositoryMockModule.forRoot(), + TokenAuthenticationModule.forRoot(), + ], + }).compile(); + + service = module.get(RegistrationService); + accountFactoryService = module.get(AccountFactoryService); + accountService = module.get(AccountService); + mailVerificationService = module.get(APIHUB24_MAIL_SERVICE); + + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("registerAccount", () => { + it("should register a new account and return it", async () => { + jest + .spyOn(accountFactoryService, "createFromRegistration") + .mockResolvedValue(mockAccount); + jest.spyOn(accountService, "save").mockResolvedValue(mockAccount); + + const result = await service.registerAccount(mockRegistration); + + expect(accountFactoryService.createFromRegistration).toHaveBeenCalledWith( + mockRegistration + ); + expect(accountService.save).toHaveBeenCalledWith(mockAccount); + expect(result).toEqual(mockAccount); + }); + + it("should return null if account saving fails", async () => { + jest + .spyOn(accountFactoryService, "createFromRegistration") + .mockResolvedValue(mockAccount); + jest.spyOn(accountService, "save").mockResolvedValue(null); + + const result = await service.registerAccount(mockRegistration); + + expect(result).toBeNull(); + }); + }); + + describe("verify", () => { + it("should verify an email and update unverified accounts", async () => { + const unverifiedAccount = { ...mockAccount, emailVerified: false }; + const verifiedAccount = { ...mockAccount, emailVerified: true }; + jest.spyOn(mailVerificationService, "verify").mockResolvedValue(true); + jest + .spyOn(accountService, "getBy") + .mockResolvedValue([unverifiedAccount]); + jest.spyOn(accountService, "save").mockResolvedValue(verifiedAccount); + + const result = await service.verify("test@example.com", "123456"); + + expect(mailVerificationService.verify).toHaveBeenCalledWith( + "test@example.com", + "123456" + ); + expect(accountService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(accountService.save).toHaveBeenCalledWith( + expect.objectContaining({ emailVerified: true }) + ); + expect(result).toBe(true); + }); + + it("should return false if verification code is invalid", async () => { + jest.spyOn(mailVerificationService, "verify").mockResolvedValue(false); + + const result = await service.verify("test@example.com", "invalid-code"); + + expect(mailVerificationService.verify).toHaveBeenCalledWith( + "test@example.com", + "invalid-code" + ); + expect(result).toBe(false); + }); + }); + + describe("activateAccount", () => { + it("should activate an account and return true", async () => { + const activeAccount = { ...mockAccount, active: true }; + jest.spyOn(accountService, "getBy").mockResolvedValue([mockAccount]); + jest.spyOn(accountService, "save").mockResolvedValue(activeAccount); + + const result = await service.activateAccount("account1"); + + expect(accountService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(accountService.save).toHaveBeenCalledWith( + expect.objectContaining({ active: true }) + ); + expect(result).toBe(true); + }); + + it("should return false if the account is not found", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([]); + + const result = await service.activateAccount("non-existent-id"); + + expect(accountService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(result).toBe(false); + }); + }); + + describe("deactivateAccount", () => { + it("should deactivate an account and return true", async () => { + const deactivatedAccount = { ...mockAccount, active: false }; + jest.spyOn(accountService, "getBy").mockResolvedValue([mockAccount]); + jest.spyOn(accountService, "save").mockResolvedValue(deactivatedAccount); + + const result = await service.deactivateAccount("account1"); + + expect(accountService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(accountService.save).toHaveBeenCalledWith( + expect.objectContaining({ active: false }) + ); + expect(result).toBe(true); + }); + + it("should return true if the account is not found", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([]); + + const result = await service.deactivateAccount("non-existent-id"); + + expect(accountService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(result).toBe(true); + }); + }); + + describe("unregisterAccount", () => { + it("should delete an account if it exists", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([mockAccount]); + jest.spyOn(accountService, "delete").mockResolvedValue(true); + + await service.unregisterAccount("account1"); + + expect(accountService.getBy).toHaveBeenCalledWith(expect.any(Function)); + expect(accountService.delete).toHaveBeenCalledWith(expect.any(Function)); + }); + + it("should do nothing if the account is not found", async () => { + jest.spyOn(accountService, "getBy").mockResolvedValue([]); + + await service.unregisterAccount("non-existent-id"); + + expect(accountService.getBy).toHaveBeenCalledWith(expect.any(Function)); + }); + }); +}); diff --git a/src/services/right.factory.service.spec.ts b/src/services/right.factory.service.spec.ts new file mode 100644 index 0000000..0df13ee --- /dev/null +++ b/src/services/right.factory.service.spec.ts @@ -0,0 +1,70 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { RightFactoryService } from "./right.factory.service"; +import { MailServiceMockModule } from "../../test/mail.verification.service.mock"; +import { PasswordServiceMockModule } from "../../test/password.service.mock"; +import { SessionServiceMockModule } from "../../test/session.service.mock"; +import { TokenServiceMockModule } from "../../test/token.service.mock"; +import { AccountRepositoryMockModule } from "../../test/account.repository.mock"; +import { GroupRepositoryMockModule } from "../../test/group.repository.mock"; +import { RightRepositoryMockModule } from "../../test/right.repository.mock"; +import { OrganizationRepositoryMockModule } from "../../test/organization.repository.mock"; +import { TokenAuthenticationModule } from ".."; +import * as validator from "class-validator"; +import { Right } from "../models/right"; + +describe("RightFactoryService Tests", () => { + let module: TestingModule; + let service: RightFactoryService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MailServiceMockModule.forRoot(), + PasswordServiceMockModule.forRoot(), + SessionServiceMockModule.forRoot(), + TokenServiceMockModule.forRoot(), + AccountRepositoryMockModule.forRoot(), + GroupRepositoryMockModule.forRoot(), + RightRepositoryMockModule.forRoot(), + OrganizationRepositoryMockModule.forRoot(), + TokenAuthenticationModule.forRoot(), + ], + }).compile(); + + service = module.get(RightFactoryService); + + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("createRight", () => { + it("should create a right successfully with a valid name", async () => { + const rightName = "read:articles"; + jest.spyOn(validator, "validate").mockResolvedValue([]); + + const result = await service.createRight(rightName); + + expect(validator.validate).toHaveBeenCalled(); + expect(result).toBeInstanceOf(Right); + expect(result.name).toBe(rightName); + }); + + it("should throw an error if validation fails", async () => { + const rightName = ""; + const validationError = [ + { + toString: () => "name must not be empty", + } as validator.ValidationError, + ]; + jest.spyOn(validator, "validate").mockResolvedValue(validationError); + + await expect(service.createRight(rightName)).rejects.toThrow( + "name must not be empty" + ); + expect(validator.validate).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/services/right.service.spec.ts b/src/services/right.service.spec.ts new file mode 100644 index 0000000..c9e474f --- /dev/null +++ b/src/services/right.service.spec.ts @@ -0,0 +1,113 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { RightService } from "./right.service"; +import { IRepository } from "@apihub24/repository"; +import { APIHUB24_RIGHT_REPOSITORY, IRight } from "@apihub24/authentication"; +import { MailServiceMockModule } from "../../test/mail.verification.service.mock"; +import { PasswordServiceMockModule } from "../../test/password.service.mock"; +import { SessionServiceMockModule } from "../../test/session.service.mock"; +import { TokenServiceMockModule } from "../../test/token.service.mock"; +import { AccountRepositoryMockModule } from "../../test/account.repository.mock"; +import { GroupRepositoryMockModule } from "../../test/group.repository.mock"; +import { RightRepositoryMockModule } from "../../test/right.repository.mock"; +import { OrganizationRepositoryMockModule } from "../../test/organization.repository.mock"; +import { TokenAuthenticationModule } from ".."; + +describe("RightService Tests", () => { + let module: TestingModule; + let service: RightService; + let rightRepository: IRepository; + + const mockRight: IRight = { + id: "right1", + name: "read:users", + }; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MailServiceMockModule.forRoot(), + PasswordServiceMockModule.forRoot(), + SessionServiceMockModule.forRoot(), + TokenServiceMockModule.forRoot(), + AccountRepositoryMockModule.forRoot(), + GroupRepositoryMockModule.forRoot(), + RightRepositoryMockModule.forRoot(), + OrganizationRepositoryMockModule.forRoot(), + TokenAuthenticationModule.forRoot(), + ], + }).compile(); + + service = module.get(RightService); + rightRepository = module.get(APIHUB24_RIGHT_REPOSITORY); + + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("getBy", () => { + it("should return rights that match the filter", async () => { + jest.spyOn(rightRepository, "getBy").mockResolvedValue([mockRight]); + + const result = await service.getBy((r) => r.id === "right1"); + + expect(result).toEqual([mockRight]); + expect(rightRepository.getBy).toHaveBeenCalledWith(expect.any(Function)); + }); + + it("should return an empty array if no rights match", async () => { + jest.spyOn(rightRepository, "getBy").mockResolvedValue([]); + + const result = await service.getBy((r) => r.id === "non-existent-id"); + + expect(result).toEqual([]); + expect(rightRepository.getBy).toHaveBeenCalledWith(expect.any(Function)); + }); + }); + + describe("save", () => { + it("should save a right and return it on success", async () => { + jest.spyOn(rightRepository, "save").mockResolvedValue([mockRight]); + + const result = await service.save(mockRight); + + expect(result).toEqual(mockRight); + expect(rightRepository.save).toHaveBeenCalledWith([mockRight]); + }); + + it("should return null if the save operation fails", async () => { + jest.spyOn(rightRepository, "save").mockResolvedValue([]); + + const result = await service.save(mockRight); + + expect(result).toBeNull(); + expect(rightRepository.save).toHaveBeenCalledWith([mockRight]); + }); + }); + + describe("delete", () => { + it("should return true if the deletion is successful", async () => { + jest.spyOn(rightRepository, "deleteBy").mockResolvedValue(true); + + const result = await service.delete((r) => r.id === "right1"); + + expect(result).toBe(true); + expect(rightRepository.deleteBy).toHaveBeenCalledWith( + expect.any(Function) + ); + }); + + it("should return false if the deletion fails", async () => { + jest.spyOn(rightRepository, "deleteBy").mockResolvedValue(false); + + const result = await service.delete((r) => r.id === "non-existent-id"); + + expect(result).toBe(false); + expect(rightRepository.deleteBy).toHaveBeenCalledWith( + expect.any(Function) + ); + }); + }); +});