Skip to content

Commit

Permalink
feat!: implement leveling module (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
HeadTriXz committed Jul 30, 2023
1 parent b52994e commit f91c214
Show file tree
Hide file tree
Showing 10 changed files with 51 additions and 31 deletions.
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Discord application
DISCORD_CLIENT_ID=
DISCORD_PUBLIC_KEY=
DISCORD_TOKEN=

# Database credentials
Expand Down
1 change: 0 additions & 1 deletion apps/barry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ The following environment variables **must be** set in order to run the applicat
| Environment Variable | Description |
|----------------------|-------------------------------------------------------------------------------------|
| DISCORD_CLIENT_ID | The ID of your Discord application. |
| DISCORD_PUBLIC_KEY | The public key of your Discord application. |
| DISCORD_TOKEN | The token of your Discord bot. |
| POSTGRES_HOST | The hostname or IP address of the database. (default: `localhost`) |
| POSTGRES_PORT | The port of the database. (default: `5432`) |
Expand Down
6 changes: 2 additions & 4 deletions apps/barry/tests/Application.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { createMockApplication, mockAppOptions } from "./mocks/application.js";
import { API } from "@discordjs/core";
import { Application } from "../src/Application.js";
import { Client } from "@barry/core";
import { Logger } from "@barry/logger";
import { WebSocketManager } from "@discordjs/ws";

describe("Application", () => {
let app: Application;
Expand All @@ -23,8 +21,8 @@ describe("Application", () => {
it("should initialize with the provided options", () => {
expect(app.api).toBeInstanceOf(API);
expect(app.applicationID).toBe(mockAppOptions.discord.applicationID);
expect(app.gateway).toBeInstanceOf(WebSocketManager);
expect(app.logger).toBeInstanceOf(Logger);
expect(app.gateway).toBeDefined();
expect(app.logger).toBeDefined();
expect(app.prisma).toBeDefined();
expect(app.redis).toBeDefined();
});
Expand Down
27 changes: 22 additions & 5 deletions apps/barry/tests/mocks/application.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import type { Redis } from "ioredis";

import { Module, UserCommand } from "@barry/core";

import { Application } from "../../src/Application.js";
import { GatewayIntentBits } from "@discordjs/core";
import { mockDeep } from "vitest-mock-extended";
import { prisma } from "./prisma.js";
import { redis } from "./redis.js";
import { vi } from "vitest";

vi.mock("ioredis", () => ({
Redis: vi.fn(() => redis)
}));

vi.mock("@prisma/client", async (importOriginal) => {
const original = await importOriginal<typeof import("@prisma/client")>();

return {
...original,
PrismaClient: vi.fn(() => prisma)
};
});

export class MockCommand extends UserCommand {
constructor(module: Module) {
super(module, {
Expand Down Expand Up @@ -52,13 +63,19 @@ export const mockAppOptions = {
*/
export function createMockApplication(override: Record<string, any> = {}): Application {
const app = new Application({
logger: {
debug: vi.fn(),
error: vi.fn(),
fatal: vi.fn(),
info: vi.fn(),
trace: vi.fn(),
warn: vi.fn()
},
...mockAppOptions,
...override
});

app.gateway.connect = vi.fn();
app.redis = mockDeep<Redis>();
app.prisma = prisma;

return app;
}
1 change: 1 addition & 0 deletions apps/barry/tests/mocks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./application.js";
export * from "./prisma.js";
export * from "./redis.js";
10 changes: 10 additions & 0 deletions apps/barry/tests/mocks/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Redis } from "ioredis";
import { mockDeep, mockReset } from "vitest-mock-extended";

import { beforeEach } from "vitest";

beforeEach(() => {
mockReset(redis);
});

export const redis = mockDeep<Redis>();
1 change: 0 additions & 1 deletion apps/barry/tests/modules/general/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ describe("GeneralModule", () => {

beforeEach(() => {
const client = createMockApplication();
client.logger.error = vi.fn();

Module.prototype.initialize = vi.fn(() => {
module.commands = [command];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ describe("MessageCreate Event", () => {
});

it("should ignore 'Cannot send messages to this user' errors", async () => {
const loggerSpy = vi.spyOn(event.client.logger, "error");
const response = {
code: 50007,
message: "Cannot send messages to this user"
Expand All @@ -132,18 +131,17 @@ describe("MessageCreate Event", () => {

await event.execute(message);

expect(loggerSpy).not.toHaveBeenCalled();
expect(event.client.logger.error).not.toHaveBeenCalled();
});

it("should handle errors during execution to prevent cooldowns not being set", async () => {
const loggerSpy = vi.spyOn(event.client.logger, "error");
const cooldownSpy = vi.spyOn(event.client.cooldowns, "set");

vi.spyOn(event.module, "checkLevel").mockRejectedValue(new Error("Oh no!"));

await event.execute(message);

expect(loggerSpy).toHaveBeenCalledOnce();
expect(event.client.logger.error).toHaveBeenCalledOnce();
expect(cooldownSpy).toHaveBeenCalledOnce();
});
});
Expand Down
27 changes: 12 additions & 15 deletions apps/barry/tests/modules/leveling/events/voiceStateUpdate.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { GatewayVoiceState } from "@discordjs/core";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { prisma, redis } from "../../../mocks/index.js";

import { DiscordAPIError } from "@discordjs/rest";
import { createMockApplication } from "../../../mocks/application.js";
import { mockMember } from "@barry/testing";
import { prisma } from "../../../mocks/index.js";

import LevelingModule from "../../../../src/modules/leveling/index.js";
import VoiceStateUpdateEvent from "../../../../src/modules/leveling/events/voiceStateUpdate.js";
Expand Down Expand Up @@ -68,7 +68,7 @@ describe("VoiceStateUpdate Event", () => {
await event.execute(state, channelID);

expect(incrementSpy).not.toHaveBeenCalled();
expect(event.client.redis.set).not.toHaveBeenCalled();
expect(redis.set).not.toHaveBeenCalled();
});

it("should ignore if the user has not left, joined, or moved to a different channel", async () => {
Expand All @@ -77,17 +77,17 @@ describe("VoiceStateUpdate Event", () => {
await event.execute({ ...state, channel_id: channelID }, channelID);

expect(incrementSpy).not.toHaveBeenCalled();
expect(event.client.redis.set).not.toHaveBeenCalled();
expect(redis.set).not.toHaveBeenCalled();
});

it("should set the voice start time when a user joins a voice channel", async () => {
await event.execute({ ...state, channel_id: channelID });

expect(event.client.redis.set).toHaveBeenCalledOnce();
expect(redis.set).toHaveBeenCalledOnce();
});

it("should update voice minutes for the user when they leave a voice channel", async () => {
vi.mocked(event.client.redis.get).mockResolvedValue("1690543918340");
vi.mocked(redis.get).mockResolvedValue("1690543918340");
const incrementSpy = vi.spyOn(event.module.memberActivity, "increment");

await event.execute(state, channelID);
Expand All @@ -100,7 +100,7 @@ describe("VoiceStateUpdate Event", () => {
});

it("should check if the user has leveled up if the previous channel is known", async () => {
vi.mocked(event.client.redis.get).mockResolvedValue("1690543918340");
vi.mocked(redis.get).mockResolvedValue("1690543918340");
const checkLevelSpy = vi.spyOn(event.module, "checkLevel");
const incrementSpy = vi.spyOn(event.module.memberActivity, "increment");

Expand All @@ -111,7 +111,7 @@ describe("VoiceStateUpdate Event", () => {
});

it("should not check if the user has leveled up if the previous channel is unknown", async () => {
vi.mocked(event.client.redis.get).mockResolvedValue("1690543918340");
vi.mocked(redis.get).mockResolvedValue("1690543918340");
const checkLevelSpy = vi.spyOn(event.module, "checkLevel");
const incrementSpy = vi.spyOn(event.module.memberActivity, "increment");

Expand Down Expand Up @@ -152,7 +152,7 @@ describe("VoiceStateUpdate Event", () => {
});

it("should not update voice minutes if the start time is not cached", async () => {
vi.mocked(event.client.redis.get).mockResolvedValue(null);
vi.mocked(redis.get).mockResolvedValue(null);
const incrementSpy = vi.spyOn(event.module.memberActivity, "increment");

await event.execute(state, channelID);
Expand All @@ -161,31 +161,28 @@ describe("VoiceStateUpdate Event", () => {
});

it("should ignore 'Cannot send messages to this user' errors", async () => {
const loggerSpy = vi.spyOn(event.client.logger, "error");
const response = {
code: 50007,
message: "Cannot send messages to this user"
};

const error = new DiscordAPIError(response, 50007, 200, "GET", "", {});

vi.mocked(event.client.redis.get).mockResolvedValue("1690543918340");
vi.mocked(redis.get).mockResolvedValue("1690543918340");
vi.spyOn(event.module, "checkLevel").mockRejectedValue(error);

await event.execute(state, channelID);

expect(loggerSpy).not.toHaveBeenCalledOnce();
expect(event.client.logger.error).not.toHaveBeenCalledOnce();
});

it("should handle errors during execution", async () => {
const loggerSpy = vi.spyOn(event.client.logger, "error");

vi.mocked(event.client.redis.get).mockResolvedValue("1690543918340");
vi.mocked(redis.get).mockResolvedValue("1690543918340");
vi.spyOn(event.module, "checkLevel").mockRejectedValue(new Error("Oh no!"));

await event.execute(state, channelID);

expect(loggerSpy).toHaveBeenCalledOnce();
expect(event.client.logger.error).toHaveBeenCalledOnce();
});
});
});
2 changes: 2 additions & 0 deletions packages/core/src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
import {
type API,
type APIInteraction,
type GatewayDispatchPayload,
type GatewayVoiceState,
type MappedEvents,
type WithIntrinsicProps,
type GatewayVoiceState,
Expand Down

0 comments on commit f91c214

Please # to comment.