diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..9cc5554c9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: Test +on: + push: + branches: + - master +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: denolib/setup-deno@master + - name: Cache dependencies + run: deno cache mod.ts test/mod.test.ts + - name: Run test script + run: deno test -A \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d0ae29732..f5003566b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "deno.import_intellisense_origins": { "https://deno.land": true }, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, "editor.defaultFormatter": "denoland.vscode-deno" } diff --git a/README.md b/README.md index 73411f7bc..bd5888489 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ ![Testing/Linting](https://github.com/Skillz4Killz/Discordeno/workflows/Testing/Linting/badge.svg) [![nest badge](https://nest.land/badge.svg)](https://nest.land/package/Discordeno) +[WIP] ![Test](https://github.com/Skillz4Killz/Discordeno/workflows/Test/badge.svg) + ## Why Discordeno? ### Beginner Developers diff --git a/deps.ts b/deps.ts index 776b73054..ddc71fbf2 100644 --- a/deps.ts +++ b/deps.ts @@ -7,4 +7,9 @@ export { isWebSocketPongEvent, } from "https://deno.land/std@0.67.0/ws/mod.ts"; export type { WebSocket } from "https://deno.land/std@0.67.0/ws/mod.ts"; +export { + assert, + assertArrayIncludes, + assertEquals, +} from "https://deno.land/std@0.75.0/testing/asserts.ts"; export { decompress_with as inflate } from "https://unpkg.com/@evan/wasm@0.0.11/target/zlib/deno.js"; diff --git a/src/controllers/messages.ts b/src/controllers/messages.ts index fe45ed022..a0e72dfdc 100644 --- a/src/controllers/messages.ts +++ b/src/controllers/messages.ts @@ -15,9 +15,6 @@ export async function handleInternalMessageCreate(data: DiscordPayload) { const channel = await cacheHandlers.get("channels", payload.channel_id); if (channel) channel.lastMessageID = payload.id; - const message = await structures.createMessage(payload); - // Cache the message - cacheHandlers.set("messages", payload.id, message); const guild = payload.guild_id ? await cacheHandlers.get("guilds", payload.guild_id) : undefined; @@ -46,6 +43,10 @@ export async function handleInternalMessageCreate(data: DiscordPayload) { } }); + const message = await structures.createMessage(payload); + // Cache the message + cacheHandlers.set("messages", payload.id, message); + eventHandlers.messageCreate?.(message); } diff --git a/src/handlers/guild.ts b/src/handlers/guild.ts index 5ef7b5f07..b045affe6 100644 --- a/src/handlers/guild.ts +++ b/src/handlers/guild.ts @@ -32,7 +32,7 @@ import { Permissions } from "../types/permission.ts"; import type { RoleData } from "../types/role.ts"; import { formatImageURL } from "../utils/cdn.ts"; import { Collection } from "../utils/collection.ts"; -import { botHasPermission } from "../utils/permissions.ts"; +import { botHasPermission, calculateBits } from "../utils/permissions.ts"; import { urlToBase64 } from "../utils/utils.ts"; /** Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. This endpoint can be used only by bots in less than 10 guilds. */ @@ -317,7 +317,12 @@ export function editRole( ) { throw new Error(Errors.MISSING_MANAGE_ROLES); } - return RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), options); + return RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), { + ...options, + permissions: options.permissions + ? calculateBits(options.permissions) + : undefined, + }); } /** Delete a guild role. Requires the MANAGE_ROLES permission. */ diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 6de579299..f69ec570b 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -188,6 +188,7 @@ export async function hasChannelPermissions( return botHasPermission(guild.id, permissions); } +/** This function converts a bitwise string to permission strings */ export function calculatePermissions(permissionBits: bigint) { return Object.keys(Permissions).filter((perm) => { if (typeof perm !== "number") return false; @@ -195,6 +196,14 @@ export function calculatePermissions(permissionBits: bigint) { }) as Permission[]; } +/** This function converts an array of permissions into the bitwise string. */ +export function calculateBits(permissions: Permission[]) { + return permissions.reduce( + (bits, perm) => bits |= BigInt(Permissions[perm]), + BigInt(0), + ).toString(); +} + export async function highestRole(guildID: string, memberID: string) { const guild = await cacheHandlers.get("guilds", guildID); if (!guild) return; diff --git a/tests/mod.test.ts b/tests/mod.test.ts new file mode 100644 index 000000000..bfa3169e7 --- /dev/null +++ b/tests/mod.test.ts @@ -0,0 +1,238 @@ +import { assert, assertArrayIncludes, assertEquals, delay } from "../deps.ts"; +import { + botID, + cache, + Channel, + createClient, + createGuildChannel, + createGuildRole, + createServer, + deleteChannel, + deleteRole, + deleteServer, + editRole, + getMessage, + Guild, + Intents, + OverwriteType, + Role, + sendMessage, +} from "../mod.ts"; +import { editChannel } from "../src/handlers/channel.ts"; +import { getChannel } from "../src/handlers/guild.ts"; + +// TODO: add DISCORD_TOKEN variable to GitHub secrets +const token = Deno.env.get("DISCORD_TOKEN"); +if (!token) throw "Token is not provided"; + +createClient({ + token, + intents: [Intents.GUILD_MESSAGES, Intents.GUILDS], +}); + +// Default options for all test cases +const testOptions = { + sanitizeOps: false, + sanitizeResources: false, +}; + +Deno.test({ + name: "connect to the gateway", + fn: async () => { + // Delay the execution by 15 seconds (15000 ms) + await delay(15000); + + // Check whether botID is nil or not + assert(botID); + }, + ...testOptions, +}); + +const data = { + guildID: "", + roleID: "", + channelID: "", +}; + +Deno.test({ + name: "create a guild", + async fn() { + // Create a guild "Discordeno Test" + const createdGuild = (await createServer({ + name: "Discordeno Test", + })) as Guild; + + // Check whether createdGuild is nil or not + assert(createdGuild); + + data.guildID = createdGuild.id; + }, + ...testOptions, +}); + +// Role + +Deno.test({ + name: "create a role in a guild", + async fn() { + // Create a role "Role 1" in the guild "Discordeno Test" + const createdRole = await createGuildRole(data.guildID, { + name: "Role 1", + }); + + // Check whether the created role is nil or not + assert(createdRole); + + data.roleID = createdRole.id; + }, + ...testOptions, +}); + +Deno.test({ + name: "edit a role in a guild", + async fn() { + // Edit a role "Role 1" in the guild "Discordeno Test" + const editedRole = (await editRole(data.guildID, data.roleID, { + name: "Edited Role", + color: 4320244, + hoist: false, + mentionable: false, + })) as Role; + + // Assertions + assert(editedRole); + assertEquals(editedRole.name, "Edited Role"); + assertEquals(editedRole.color, 4320244); + assertEquals(editedRole.hoist, false); + assertEquals(editedRole.mentionable, false); + + data.roleID = editedRole.id; + }, + ...testOptions, +}); + +// Channel + +Deno.test({ + name: "create a channel in a guild", + async fn() { + const guild = cache.guilds.get(data.guildID); + if (!guild) throw "Guild not found"; + const createdChannel = await createGuildChannel(guild, "test"); + + // Check whether the created channel is nil or not + assert(createdChannel); + + data.channelID = createdChannel.id; + }, + ...testOptions, +}); + +Deno.test({ + name: "get a channel in a guild", + async fn() { + const channel = await getChannel(data.channelID); + + assertEquals(channel.id, data.channelID); + }, + ...testOptions, +}); + +Deno.test({ + name: "edit a channel in a guild", + async fn() { + const channel = await editChannel(data.channelID, { + name: "edited channel", + }) as Channel; + + assert(channel); + + data.channelID = channel.id; + }, +}); + +Deno.test({ + name: "channel overwrite has permission", + async fn() { + const channel = cache.channels.get(data.channelID); + if (!channel) throw "Channel not found"; + assertArrayIncludes(channel.permission_overwrites!, [ + { + id: data.roleID, + type: OverwriteType.ROLE, + // The type for Channel#permission_overwrites is "RawOverwrite[] | undefined" + // not "Overwrite[]"; therefore, permission strings cannot be used. + // allow: ["VIEW_CHANNEL", "SEND_MESSAGES"], + // deny: ["USE_EXTERNAL_EMOJIS"], + }, + ]); + + // THIS TEST CASE SHOULD BE REFACTORED AND IMPROVED + // CURRENTLY, IT USES Channel#permission_overwrites + // but preferably, it should use the channelOverwriteHasPermission() + }, + ...testOptions, +}); + +// Message + +let messageID: string; + +Deno.test({ + name: "create a message in a guild", + async fn() { + const createdMessage = await sendMessage(data.channelID, "test"); + + // Check whether the created message is nil or not + assert(createdMessage); + + messageID = createdMessage.id; + }, +}); + +Deno.test({ + name: "get a message in a guild", + async fn() { + const message = await getMessage(data.channelID, messageID); + + assertEquals(messageID, message.id); + }, +}); + +// Clean up + +Deno.test({ + name: "delete a role from the guild", + async fn() { + await deleteRole(data.guildID, data.roleID); + data.roleID = ""; + assertEquals(data.roleID, ""); + }, +}); + +Deno.test({ + name: "delete a channel in the guild", + async fn() { + await deleteChannel(data.guildID, data.channelID); + }, + ...testOptions, +}); + +Deno.test({ + name: "delete a guild", + async fn() { + await deleteServer(data.guildID); + data.guildID = ""; + assertEquals(data.guildID, ""); + }, + ...testOptions, +}); + +// This is meant to be the final test that forcefully crashes the bot +Deno.test({ + name: "exit the process forcefully after all the tests are done", + async fn() { + Deno.exit(1); + }, + ...testOptions, +});