diff --git a/src/core/http.ts b/src/core/http.ts index 4a29264..e7a12eb 100644 --- a/src/core/http.ts +++ b/src/core/http.ts @@ -1,25 +1,12 @@ -import axios, { AxiosInstance } from 'axios'; +import axios, {AxiosInstance} from 'axios'; import {environment} from "./environment"; -let client: AxiosInstance; - export const getClient = () => { - if (!client) { - const env = environment.get(); + const env = environment.get(); - client = axios.create({ - baseURL: env.apiUrl, - timeout: 1000, - headers: { 'Content-Type': 'application/json' } - }); - - /*client.interceptors.request.use((config) => { - const token = localStorage.getItem('token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - });*/ - } - return client; + return axios.create({ + baseURL: env.apiUrl, + timeout: 5000, + headers: {'Content-Type': 'application/json'} + }); }; \ No newline at end of file diff --git a/src/domain/common.schema.ts b/src/domain/common.schema.ts index c4574cc..565b794 100644 --- a/src/domain/common.schema.ts +++ b/src/domain/common.schema.ts @@ -7,4 +7,11 @@ export interface RGB { export interface TimeStamp { T: number; I: number; +} + +export interface PublicUserData { + pfp: string + displayName: string + username: string + userid: string } \ No newline at end of file diff --git a/src/domain/networkService.schema.ts b/src/domain/networkService.schema.ts index eb74d9a..2b0e764 100644 --- a/src/domain/networkService.schema.ts +++ b/src/domain/networkService.schema.ts @@ -1,5 +1,5 @@ // Request schemas -import {RGB, TimeStamp} from "./common.schema"; +import {PublicUserData, RGB, TimeStamp} from "./common.schema"; export interface GetInvitesReq { networkId: string @@ -166,6 +166,13 @@ export interface EditRankReq { rankId: string } +export interface DeleteChannelReq { + userid: string + categoryId: string + networkId: string + channelId: string +} + export interface DeleteRankReq { userid: string networkId: string @@ -195,7 +202,7 @@ export interface AssignRankToMemberReq { networkId: string } -export interface GetMemberReq { +export interface GetMembersReq { userid: string networkId: string } @@ -350,6 +357,14 @@ export interface NetworkDiscovery { networkId: string } +export interface DetailedMemberList { + rankId: string + name: string + color: RGB + icon: string + members: PublicUserData[] +} + // WebSocket payloads export interface WSCategoryDeletedPayload { categoryId: string @@ -448,4 +463,29 @@ export interface WSNewNamePayload { export interface WSNewChannelPayload { in: string channel: NetworkChannel +} + +// Constants +export const NetworkPermissions = { + createAndEditCategories: 2, + deleteCategories: 4, + createAndEditChannels: 8, + deleteChannels: 16, + deleteAnyMessage: 32, + pinMessages: 64, + createAndEditRanks: 128, + deleteRanks: 256, + changeNetworkNamePictureAndVisibility: 512, + createEmojis: 1024, + deleteEmojis: 2048, + manageEmbed: 4096, + createWebhooks: 8192, + deleteWebhooks: 16384, + createInvites: 32768, + deleteInvites: 65536, + sendMessages: 131072, + seeChannels: 262144, + banMembers: 524288, + kickMembers: 1048576, + unAndAssignRanksToMember: 2097152, } \ No newline at end of file diff --git a/src/mocks/handlers/auth.http.ts b/src/mocks/handlers/auth.http.ts index 94f1317..cb64d17 100644 --- a/src/mocks/handlers/auth.http.ts +++ b/src/mocks/handlers/auth.http.ts @@ -8,7 +8,7 @@ import { } from "../../domain/authService.schema"; import {GenericSuccessBody} from "../../domain/http.schema"; -export const authHandlers = [ +export const networkHandlers = [ http.get('*/user/authOptions', () => { return HttpResponse.json({ email: true, diff --git a/src/mocks/handlers/network.http.ts b/src/mocks/handlers/network.http.ts new file mode 100644 index 0000000..5173f23 --- /dev/null +++ b/src/mocks/handlers/network.http.ts @@ -0,0 +1,74 @@ +import {http, HttpResponse} from "msw"; +import { + CreateCategoryReq, CreateInviteReq, + CreateNetworkReq, DetailedMemberList, + Network, + NetworkCategory, + NetworkInvite, POW +} from "../../domain/networkService.schema"; +import {PublicUserData} from "../../domain/common.schema"; + +export const authHandlers = [ + http.get('*/network/invites', () => { + return HttpResponse.json([ + { + inviteId: "inviteId" + } + ]) + }), + + http.post('*/network/create', async ({request}) => { + const body = await request.json() as CreateNetworkReq + return HttpResponse.json({ + name: body.name, + visibility: body.visibility + }) + }), + + http.post('*/network/get', async () => { + return HttpResponse.json([{ + name: "Test network" + }]) + }), + + http.post('*/network/createCategory', async ({request}) => { + const body = await request.json() as CreateCategoryReq + return HttpResponse.json({ + name: body.name, + desc: body.description + }) + }), + + http.post('*/network/createInvite', async ({request}) => { + const body = await request.json() as CreateInviteReq + return HttpResponse.json({ + onetime: body.times == "once" + }) + }), + + http.patch('*/network/getOverwrites', async ({request}) => { + return HttpResponse.json([{ + permissionNumber: 5000 + }]) + }), + + http.patch('*/network/getChannelOverwrites', async ({request}) => { + return HttpResponse.json([{ + permissionNumber: 5000 + }]) + }), + + http.patch('*/network/getBannedMembers', async ({request}) => { + return HttpResponse.json([{ + username: "bob", + displayName: "Bob", + }]) + }), + + http.patch('*/network/getMembers', async ({request}) => { + return HttpResponse.json([{ + name: "Test rank", + members: [{displayName: "Bob"}] + }]) + }), +] \ No newline at end of file diff --git a/src/mocks/index.ts b/src/mocks/index.ts index d750fb8..7ac4328 100644 --- a/src/mocks/index.ts +++ b/src/mocks/index.ts @@ -1,5 +1,7 @@ -import {authHandlers} from "./handlers/auth.http"; +import {networkHandlers} from "./handlers/auth.http"; +import {authHandlers} from "./handlers/network.http"; export const allHandlers = [ - ...authHandlers + ...authHandlers, + ...networkHandlers ] \ No newline at end of file diff --git a/src/mocks/storage/database.ts b/src/mocks/storage/database.ts new file mode 100644 index 0000000..c103aab --- /dev/null +++ b/src/mocks/storage/database.ts @@ -0,0 +1,21 @@ +import {DatabaseAPI} from "../../storage/database"; + +export class DatabaseMock implements DatabaseAPI { + database: { [collection: string]: { [key: string]: string } } = {}; + + set(collection: string, key: string, value: any) { + this.database[collection][key] = JSON.stringify(value); + } + + get(collection: string, key: string): any { + return this.database[collection][key] + } + + delete(collection: string, key: string) { + delete this.database[collection][key]; + } + + flush() { + this.database = {}; + } +} \ No newline at end of file diff --git a/src/services/authService.test.ts b/src/services/authService.test.ts index 1fd47b0..33d8cc3 100644 --- a/src/services/authService.test.ts +++ b/src/services/authService.test.ts @@ -4,8 +4,9 @@ import {VerificationTypeEmail} from "../domain/authService.schema"; import {faker} from "@faker-js/faker/locale/en"; describe("AuthService", () => { + const service = new AuthService(); + it("should return authMethods", async () => { - const service = new AuthService(); const methods = await service.getAuthMethods("") expect(methods.sms).toBeFalsy() expect(methods.email).toBeTruthy() @@ -13,57 +14,47 @@ describe("AuthService", () => { }) it("should send OTP code", async () => { - const service = new AuthService(); const code = await service.otpSendCode("bob@example.com", VerificationTypeEmail) expect(code).toBe(5000) }) it("should send password reset code", async () => { - const service = new AuthService(); const code = await service.resetPassword("bob@example.com") expect(code).toBe(5000) }) it("should send PLE code", async () => { - const service = new AuthService(); const code = await service.pleSendVCode("bob@example.com", VerificationTypeEmail) expect(code).toBe(5000) }) it("should throw on wrong OTP code", async () => { - const service = new AuthService(); await expect(service.otpVerifyCode("bob@example.com", VerificationTypeEmail, 9999)).rejects.toThrow() }) it("should throw on wrong password reset code", async () => { - const service = new AuthService(); await expect(service.verifyPasswordReset("bob@example.com", 9999, "newPassword")).rejects.toThrow() }) it("should throw on wrong PLE code", async () => { - const service = new AuthService(); await expect(service.pleVerifyCode("bob@example.com", VerificationTypeEmail, 9999)).rejects.toThrow() }) it("should throw on wrong password", async () => { - const service = new AuthService(); await expect(service.loginPasswordAuth("bob@example.com", "wrongPasswd")).rejects.toThrow(); }) it("should detect taken username", async () => { - const service = new AuthService(); const taken = await service.isUsernameUsed("username") expect(taken).toBeTruthy() }) it("should detect taken e-mail address", async () => { - const service = new AuthService(); const taken = await service.isEmailUsed("taken@example.com") expect(taken).toBeTruthy() }) it("should register passwordless account", async () => { - const service = new AuthService(); const username = faker.internet.username() const displayName = faker.internet.displayName() const userData = await service.finishPLEAccount( diff --git a/src/services/authService.ts b/src/services/authService.ts index eb76e82..ac55ccc 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -164,6 +164,7 @@ export class AuthService { }); return resp.data.authCode } catch (e) { + console.log(e) if (isAxiosError(e)) { throw e; } diff --git a/src/services/networkService.test.ts b/src/services/networkService.test.ts new file mode 100644 index 0000000..be5a747 --- /dev/null +++ b/src/services/networkService.test.ts @@ -0,0 +1,66 @@ +import {describe, expect, it} from "vitest"; +import {NetworkService} from "./networkService"; +import {DatabaseMock} from "../mocks/storage/database"; +import {faker} from "@faker-js/faker/locale/en"; +import {getClient} from "../core/http"; +import {environment, SDKConfig} from "../core/environment"; + +describe("NetworkService", () => { + const service = new NetworkService("", "", "", new DatabaseMock(), getClient()) + + it('should get invites', async () => { + const invites = await service.getInvites(); + expect(invites[0].inviteId).toBe("inviteId") + }); + + it('should create a network', async () => { + const netName = faker.internet.displayName() + const network = await service.create( + netName, + "private", + null + ); + expect(network.name).toBe(netName) + expect(network.visibility).toBe("private") + }); + + it('should create a category', async () => { + const catName = faker.internet.displayName() + const catDesc = faker.lorem.sentence() + const category = await service.createCategory( + catName, + catDesc + ); + expect(category.name).toBe(catName) + expect(category.desc).toBe(catDesc) + }); + + it('should create an invite', async () => { + const invite = await service.createInvite( + "once", + ); + expect(invite.onetime).toBeTruthy() + }); + + it('should get permission overwrites', async () => { + const ow = await service.getOverwrites("", ""); + expect(ow[0].permissionNumber).toBe(5000) + }); + + it('should get channel permission overwrites', async () => { + const ow = await service.getChannelOverwrites("", "", ""); + expect(ow[0].permissionNumber).toBe(5000) + }); + + it('should get banned members', async () => { + const bannedMembers = await service.getBannedMembers(); + expect(bannedMembers[0].username).toBe("bob") + expect(bannedMembers[0].displayName).toBe("Bob") + }); + + it('should get the detailed member list', async () => { + const members = await service.getMembers(); + expect(members[0].name).toBe("Test rank") + expect(members[0].members[0].displayName).toBe("Bob") + }); +}) \ No newline at end of file diff --git a/src/services/networkService.ts b/src/services/networkService.ts index 20d0647..1bb6cf7 100644 --- a/src/services/networkService.ts +++ b/src/services/networkService.ts @@ -1,4 +1,884 @@ +import {DatabaseAPI} from "../storage/database"; +import {AuthMethods} from "../domain/authService.schema"; +import {getClient} from "../core/http"; +import {AxiosInstance, isAxiosError} from "axios"; +import {GenericErrorBody} from "../domain/http.schema"; +import { + AcceptInviteReq, AssignRankToMemberReq, BanMemberReq, ChangeVisibilityReq, CreateCategoryReq, CreateChannelReq, + CreateInviteReq, + CreateNetworkReq, CreateRankReq, DeleteCategoryReq, + DeleteChannelReq, DeleteNetworkReq, DeleteRankReq, + DetailedMemberList, EditCategoryReq, EditChannelReq, EditNameReq, EditRankReq, + GetBannedMembersReq, GetChannelOverwritesReq, GetMembersReq, + GetNetworksReq, GetOverwritesReq, JoinPublicNetworkReq, JoinWebSocketRoomReq, KickMemberReq, LeaveNetworkReq, + ModifyPermissionsReq, MoveCategoryReq, MoveRankReq, + Network, NetworkCategory, NetworkChannel, NetworkDiscovery, + NetworkInvite, NetworkRank, OverwriteChannelPermissionReq, OverwritePermissionReq, PermissionUpdate, POW, + RemoveRankFromMemberReq, ToggleCategoryMuteReq, ToggleChannelNetworkMuteReq, ToggleNetworkMuteReq, + UnbanMemberReq, UploadNewPictureReq +} from "../domain/networkService.schema"; +import {PublicUserData, RGB} from "../domain/common.schema"; +import {http} from "msw"; + export class NetworkService { - constructor(networkId: string) { + userid: string; + networkId: string; + database: DatabaseAPI; + client: AxiosInstance + + constructor(userid: string, token: string, networkId: string, database: DatabaseAPI, httpClientOverwrite: AxiosInstance | null) { + this.userid = userid; + this.networkId = networkId; + this.database = database; + this.client = (httpClientOverwrite ?? getClient()).create({ + headers: { + "Authorization": token + } + }) + } + + /** + * Fetches all invites created for the network + */ + async getInvites(): Promise { + try { + const resp = await this.client.get(`network/invites?networkId=${this.networkId}&userid=${this.userid}`); + return resp.data + } catch (e) { + console.log(e) + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Creates a new network + * @param name Provided by the user + * @param visibility name Provided by the user + * @param data Optional base-64 encoded image for a custom network picture + */ + async create(name: string, visibility: "public" | "private", data: string | null): Promise { + try { + const resp = await this.client.post("network/create", { + userid: this.userid, + data: data, + name: name, + visibility: visibility, + isImage: data != null, + monogramLetter: name[0] + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Gets all networks the user is in + */ + async get(): Promise { + try { + const resp = await this.client.post("network/get", { + userid: this.userid, + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Accepts the invite and joins the network + * @param inviteId + */ + async acceptInvite(inviteId: string): Promise { + try { + const resp = await this.client.post("network/acceptInvite", { + userid: this.userid, + inviteId: inviteId, + }); + return + } catch (e) { + console.log(e) + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Joins a public network + * @param networkId + */ + async joinPublicNetwork(networkId: string): Promise { + try { + const resp = await this.client.post("network/joinNetworkDiscovery", { + userid: this.userid, + networkId: networkId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Creates a new category in the network + * @param name Provided by the user + * @param description Provided by the user + */ + async createCategory(name: string, description: string): Promise { + try { + const resp = await this.client.post("network/createCategory", { + userid: this.userid, + networkId: this.networkId, + name: name, + description: description + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Deletes the selected category + * @param categoryId + */ + async deleteCategory(categoryId: string): Promise { + try { + const resp = await this.client.patch("network/deleteCategory", { + userid: this.userid, + categoryId: categoryId, + networkId: this.networkId, + }); + return + } catch (e) { + console.log(e) + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Moves a category from a specific place to another + * @param from The category that will be moved + * @param to The place where to category will be moved to + */ + async moveCategory(from: number, to: number): Promise { + try { + await this.client.patch("network/moveCategory", { + userid: this.userid, + networkId: this.networkId, + from: from, + to: to, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Leaves the network + */ + async leave(): Promise { + try { + await this.client.post("network/leave", { + userid: this.userid, + networkId: this.networkId + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Deletes the network (Must be owned by the user) + */ + async delete(): Promise { + try { + await this.client.post("network/delete", { + userid: this.userid, + networkId: this.networkId + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Creates a new rank + * @param name Provided by the user + * @param colors Provided by the user + * @param icon Optionally provided by the user + */ + async createRank(name: string, colors: RGB, icon: string | null): Promise { + try { + const resp = await this.client.post("network/createRank", { + userid: this.userid, + networkId: this.networkId, + colors: colors, + icon: icon, + name: name, + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Creates a new invite + * @param times Whether the invite should be used once or unlimited times + */ + async createInvite(times: "once" | "unlimited"): Promise { + try { + const resp = await this.client.post("network/createInvite", { + userid: this.userid, + networkId: this.networkId, + times: times, + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Moves a rank from a specific place to another + * @param from The rank that will be moved + * @param to The place where to rank will be moved to + */ + async moveRank(from: number, to: number): Promise { + try { + const resp = await this.client.patch("network/moveRank", { + userid: this.userid, + networkId: this.networkId, + from: from, + to: to, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Kicks a specific member + * @param memberId + */ + async kickMember(memberId: string): Promise { + try { + const resp = await this.client.patch("network/kickMember", { + userid: this.userid, + networkId: this.networkId, + memberId: memberId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Bans a specific member + * @param memberId + */ + async banMember(memberId: string): Promise { + try { + const resp = await this.client.patch("network/banMember", { + userid: this.userid, + networkId: this.networkId, + memberId: memberId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Modifies the permissions of the selected rank + * @param permissionChanges + * @param rankId + */ + async modifyRankPermissions(permissionChanges: PermissionUpdate[], rankId: string): Promise { + try { + await this.client.patch("network/modifyPermissions", { + userid: this.userid, + networkId: this.networkId, + permissionChanges: permissionChanges, + rankId: rankId + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Creates a permission overwrite for a category + * @param permissionNumber + * @param to + * @param rankId + * @param categoryId + */ + async overwritePermission(permissionNumber: number, to: string, rankId: string, categoryId: string): Promise { + try { + await this.client.patch("network/overwritePermission", { + userid: this.userid, + networkId: this.networkId, + permissionNumber: permissionNumber, + rankId: rankId, + categoryId: categoryId, + category: categoryId, + to: to, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Creates a permission overwrite for a specific channel + * @param permissionNumber + * @param to + * @param rankId + * @param categoryId + * @param channelId + */ + async overwriteChannelPermission(permissionNumber: number, to: string, rankId: string, categoryId: string, channelId: string): Promise { + try { + await this.client.patch("network/overwriteChannelPermission", { + userid: this.userid, + networkId: this.networkId, + permissionNumber: permissionNumber, + rankId: rankId, + categoryId: categoryId, + to: to, + channelId: channelId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Fetches all permission overwrites made for a category + * @param rankId + * @param categoryId + */ + async getOverwrites(rankId: string, categoryId: string): Promise { + try { + const resp = await this.client.patch("network/getOverwrites", { + userid: this.userid, + networkId: this.networkId, + rankId: rankId, + categoryId: categoryId, + category: categoryId, + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + Fetches all permission overwrites made for a channel + * @param rankId + * @param categoryId + * @param channelId + */ + async getChannelOverwrites(rankId: string, categoryId: string, channelId: string): Promise { + try { + const resp = await this.client.patch("network/getChannelOverwrites", { + userid: this.userid, + networkId: this.networkId, + rankId: rankId, + categoryId: categoryId, + category: categoryId, + channelId: channelId + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Modifies the name and/or description of a channel + * @param categoryId + * @param name + * @param desc + */ + async editCategory(categoryId: string, name: string, desc: string): Promise { + try { + await this.client.patch("network/editCategory", { + userid: this.userid, + networkId: this.networkId, + categoryId: categoryId, + name: name, + desc: desc, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Unbans a specific member + * @param memberId + */ + async unbanMember(memberId: string): Promise { + try { + await this.client.patch("network/unbanMember", { + userid: this.userid, + networkId: this.networkId, + memberId: memberId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Gets all banned members public user data + */ + async getBannedMembers(): Promise { + try { + const resp = await this.client.patch("network/getBannedMembers", { + userid: this.userid, + networkId: this.networkId + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Deletes a specific rank + * @param rankId + */ + async deleteRank(rankId: string): Promise { + try { + await this.client.patch("network/deleteRank", { + userid: this.userid, + networkId: this.networkId, + rankId: rankId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Modifies the name, colors and/or the icon of a specific rank + * @param rankId + * @param name + * @param colors + * @param icon + */ + async editRank(rankId: string, name: string, colors: RGB, icon: string | null): Promise { + try { + await this.client.patch("network/editRank", { + userid: this.userid, + networkId: this.networkId, + rankId: rankId, + colors: colors, + icon: icon, + name: name, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Deletes a specific channel + * @param categoryId + * @param channelId + */ + async deleteChannel(categoryId: string, channelId: string): Promise { + try { + await this.client.patch("network/deleteChannel", { + userid: this.userid, + networkId: this.networkId, + categoryId: categoryId, + channelId: channelId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Edits a specific channel + * @param categoryId + * @param channelId + * @param name + * @param desc + */ + async editChannel(categoryId: string, channelId: string, name: string, desc: string): Promise { + try { + await this.client.patch("network/editChannel", { + userid: this.userid, + networkId: this.networkId, + categoryId: categoryId, + channelId: channelId, + name: name, + desc: desc, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Removes a rank from a specific member + * @param targetUserId + * @param rankId + */ + async removeRankFromMember(targetUserId: string, rankId: string): Promise { + try { + await this.client.patch("network/removeRankFromMember", { + userid: this.userid, + networkId: this.networkId, + rankId: rankId, + targetUserId: targetUserId, + userId: this.userid, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Assigns a rank to a specific member + * @param targetUserId + * @param rankId + */ + async assignRankToMember(targetUserId: string, rankId: string): Promise { + try { + await this.client.patch("network/assignRankToMember", { + userid: this.userid, + networkId: this.networkId, + rankId: rankId, + targetUserId: targetUserId, + userId: this.userid, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Gets a detailed member list with all ranks and members inside them + */ + async getMembers(): Promise { + try { + const resp = await this.client.patch("network/getMembers", { + userid: this.userid, + networkId: this.networkId, + }); + return resp.data + } catch (e) { + console.error(e) + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Uploads a new network picture + * @param picId + */ + async uploadNewPic(picId: string): Promise { + try { + await this.client.patch("network/uploadNewPic", { + userid: this.userid, + networkId: this.networkId, + picId: picId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Changes the visibility of the network + * @param to + */ + async changeVisibility(to: "public" | "private"): Promise { + try { + await this.client.patch("network/changeVisibility", { + userid: this.userid, + networkId: this.networkId, + to: to, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Edits the name of the network + * @param name + */ + async editName(name: string): Promise { + try { + await this.client.patch("network/editName", { + userid: this.userid, + networkId: this.networkId, + name: name, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Creates a new channel inside a category in the network + * @param categoryId + * @param type + * @param description + * @param name + */ + async createChannel(categoryId: string, type: "message" | "broadcast" | "voice", description: string, name: string): Promise { + try { + const resp = await this.client.patch("network/createChannel", { + userid: this.userid, + networkId: this.networkId, + categoryId: categoryId, + name: name, + description: description, + type: type, + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * (un)mutes the network + */ + async toggleMute(): Promise { + try { + await this.client.patch("network/toggleMute", { + userid: this.userid, + networkId: this.networkId + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * (un)mutes a category inside the network + * @param categoryId + */ + async toggleCategoryMute(categoryId: string): Promise { + try { + await this.client.patch("network/toggleCatMute", { + userid: this.userid, + networkId: this.networkId, + categoryId: categoryId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * (un)mutes a channel inside a category in the network + * @param channelId + */ + async toggleChannelMute(channelId: string): Promise { + try { + await this.client.patch("network/toggleChanMute", { + userid: this.userid, + networkId: this.networkId, + channelId: channelId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Starts listening for live changes inside the network + * @param connId + */ + async joinWebSocketRoom(connId: string): Promise { + try { + await this.client.patch("v2/network/joinWebSocketRoom", { + userid: this.userid, + networkId: this.networkId, + connId: connId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Fetches network data from an invite + * @param inviteId + */ + async getFromInvite(inviteId: string): Promise { + try { + const resp = await this.client.get(`network/fromInvite?inviteId=${inviteId}`); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Fetches network discovery (A list of public networks) + */ + async getDiscovery(): Promise { + try { + const resp = await this.client.get("network/discovery"); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } } } \ No newline at end of file diff --git a/src/storage/database.ts b/src/storage/database.ts new file mode 100644 index 0000000..f747428 --- /dev/null +++ b/src/storage/database.ts @@ -0,0 +1,6 @@ +export interface DatabaseAPI { + set(collection: string, key: string, value: any): void; + get(collection: string, key: string): string; + delete(collection: string, key: string): void; + flush(): void; +} \ No newline at end of file diff --git a/tests/networkService.test.ts b/tests/networkService.test.ts new file mode 100644 index 0000000..afc02c4 --- /dev/null +++ b/tests/networkService.test.ts @@ -0,0 +1,192 @@ +import {describe, expect, it} from "vitest"; +import {NetworkService} from "../src/services/networkService"; +import {DatabaseMock} from "../src/mocks/storage/database"; +import {environment, SDKConfig} from "../src/core/environment"; +import {getClient} from "../src/core/http"; +import {faker} from "@faker-js/faker/locale/en"; +import {AuthService} from "../src/services/authService"; +import {RGB} from "../src/domain/common.schema"; +import {NetworkPermissions, PermissionUpdate} from "../src/domain/networkService.schema"; + +const NETWORK_SERVICE_TESTING_NETWORK_ID = "000000000000000000000000" +const NETWORK_SERVICE_TESTING_USER_ID = "000000000000000000000000" +const NETWORK_SERVICE_TESTING_CATEGORY_ID = "111111111111111111111111" +const NETWORK_SERVICE_TESTING_SECONDARY_MEMBER_ID = "111111111111111111111111" +const NETWORK_SERVICE_TESTING_CHANNEL_ID = "222222222222222222222222" +const NETWORK_SERVICE_TESTING_RANK_ID = "333333333333333333333333" +const NETWORK_SERVICE_TESTING_INVITE_ID = "444444444444444444444444" +const NETWORK_SERVICE_TESTING_TOKEN = "testingToken" + +describe("NetworkService Integration Testing", () => { + environment.overwrite({apiUrl: "http://localhost:3000"}) + const service = new NetworkService( + NETWORK_SERVICE_TESTING_USER_ID, + NETWORK_SERVICE_TESTING_TOKEN, + NETWORK_SERVICE_TESTING_NETWORK_ID, + new DatabaseMock(), + getClient() + ) + + it("should get invites", async () => { + const invites = await service.getInvites() + expect(invites[0].onetime).toBeFalsy() + }) + + it("should create network", async () => { + const netName = faker.internet.displayName() + const network = await service.create( + netName, + "private", + null + ) + expect(network.name).toBe(netName) + }) + + it('should get networks', async () => { + const networks = await service.get() + expect(networks[0].name).toBe("Test Network") + }); + + it('should create category', async () => { + const catName = faker.internet.displayName() + const category = await service.createCategory( + catName, + faker.lorem.sentence(), + ) + + expect(category.name).toBe(catName) + }); + + it('should delete category', async () => { + await service.deleteCategory(NETWORK_SERVICE_TESTING_CATEGORY_ID) + const networks = await service.get() + expect(networks[0].categories.length).toBe(0) + }); + + it('should move category', async () => { + await service.createCategory("Test name", "Test desc") + await service.moveCategory(0, 1) + const networks = await service.get() + expect(networks[0].categories[1].categoryId).toBe(NETWORK_SERVICE_TESTING_CATEGORY_ID) + }); + + it('should throw when leaving self-owned network', async () => { + await expect(service.leave()).rejects.toThrow() + }); + + it('should create rank', async () => { + const rankName = faker.internet.displayName() + const rank = await service.createRank(rankName, {r: 0, g: 0, b: 0}, null) + expect(rank.name).toBe(rankName) + }); + + it('should create invite', async () => { + const invite = await service.createInvite("unlimited") + expect(invite.onetime).toBeFalsy() + }); + + it('should move rank', async () => { + await service.createRank(faker.internet.displayName(), {r: 0, g: 0, b: 0}, null) + await service.moveRank(0, 1) + const networks = await service.get() + expect(networks[0].ranks[1].name).toBe("Rank name") + }); + + it('should kick member', async () => { + await service.kickMember(NETWORK_SERVICE_TESTING_SECONDARY_MEMBER_ID) + }); + + it('should ban member', async () => { + await service.banMember(NETWORK_SERVICE_TESTING_SECONDARY_MEMBER_ID) + }); + + it('should modify rank permissions', async () => { + await service.modifyRankPermissions([ + { + granted: true, + permissionNumber: NetworkPermissions.banMembers + } + ], NETWORK_SERVICE_TESTING_RANK_ID) + const networks = await service.get() + expect(networks[0].ranks[0].permissions).toBe(NetworkPermissions.banMembers) + }); + + it('should overwrite category permissions', async () => { + await service.overwritePermission(NetworkPermissions.banMembers, "grant", NETWORK_SERVICE_TESTING_RANK_ID, NETWORK_SERVICE_TESTING_CATEGORY_ID) + const overwrites = await service.getOverwrites(NETWORK_SERVICE_TESTING_RANK_ID, NETWORK_SERVICE_TESTING_CATEGORY_ID) + expect(overwrites.length).not.toBe(0) + }); + + it('should overwrite channel permissions', async () => { + await service.overwriteChannelPermission( + NetworkPermissions.banMembers, + "grant", + NETWORK_SERVICE_TESTING_RANK_ID, + NETWORK_SERVICE_TESTING_CATEGORY_ID, + NETWORK_SERVICE_TESTING_CHANNEL_ID + ) + const overwrites = await service.getChannelOverwrites(NETWORK_SERVICE_TESTING_RANK_ID, NETWORK_SERVICE_TESTING_CATEGORY_ID, NETWORK_SERVICE_TESTING_CHANNEL_ID) + expect(overwrites.length).not.toBe(0) + }); + + it('should edit the category', async () => { + await service.editCategory(NETWORK_SERVICE_TESTING_CATEGORY_ID, "Edited name", "Edited desc") + const network = await service.get() + expect(network[0].categories[0].name).toBe("Edited name") + expect(network[0].categories[0].desc).toBe("Edited desc") + }); + + it('should edit the rank', async () => { + await service.editRank(NETWORK_SERVICE_TESTING_RANK_ID, "Edited rank name", {r: 0, g: 0, b: 0}, null) + const networks = await service.get() + expect(networks[0].ranks[0].name).toBe("Edited rank name") + }); + + it('should edit the channel', async () => { + await service.editChannel(NETWORK_SERVICE_TESTING_CATEGORY_ID, NETWORK_SERVICE_TESTING_CHANNEL_ID, "Edited channel name", "Edited channel desc") + const networks = await service.get() + expect(networks[0].categories[0].channels[0].name).toBe("Edited channel name") + expect(networks[0].categories[0].channels[0].desc).toBe("Edited channel desc") + }); + + it('should should assign and remove rank to/from a member', async () => { + await service.assignRankToMember(NETWORK_SERVICE_TESTING_USER_ID, NETWORK_SERVICE_TESTING_RANK_ID) + let networks = await service.get() + expect(networks[0].ranks[0].members.length).toBe(1) + await service.removeRankFromMember(NETWORK_SERVICE_TESTING_USER_ID, NETWORK_SERVICE_TESTING_RANK_ID) + networks = await service.get() + expect(networks[0].ranks[0].members.length).toBe(0) + }); + + it('should change the visibility', async () => { + await service.changeVisibility("private") + const networks = await service.get() + expect(networks[0].visibility).toBe("private") + }); + + it('should edit the network\'s name', async () => { + const newNetName = faker.internet.displayName() + await service.editName(newNetName) + const networks = await service.get() + expect(networks[0].name).toBe(newNetName) + }); + + it('should create a new channel', async () => { + const chanName = faker.internet.displayName() + const chanDesc = faker.lorem.sentence() + const channel = await service.createChannel(NETWORK_SERVICE_TESTING_CATEGORY_ID, "voice", chanDesc, chanName) + expect(channel.name).toBe(chanName) + expect(channel.desc).toBe(chanDesc) + expect(channel.type).toBe("voice") + }); + + it('should get network from invite', async () => { + const network = await service.getFromInvite(`${NETWORK_SERVICE_TESTING_INVITE_ID}.${NETWORK_SERVICE_TESTING_NETWORK_ID}`) + expect(network.name).toBe("Test Network") + }); + + it('should get discovery', async () => { + const network = await service.getDiscovery() + expect(network[0].name).toBe("Test Network") + }); +}) \ No newline at end of file