Skip to content

Commit

Permalink
Merge pull request #3 from TogetherCrew/project-config
Browse files Browse the repository at this point in the history
BullMQ
  • Loading branch information
cyri113 authored May 21, 2023
2 parents 98c21da + fb60b39 commit 02b3e64
Show file tree
Hide file tree
Showing 29 changed files with 1,157 additions and 31 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Note: We have disabled HADOLINT for now as we are getting an error: `qemu: uncau
The CI Pipeline uses the `test` target from the Dockerfile to run the tests. You can run it locally with the following command:

```bash
docker compose -f docker-compose.test.yml up --exit-code-from app
docker compose -f docker-compose.test.yml up --exit-code-from app --build
```

Note: This will create a /coverage folder where you can review the coverage details.
Expand All @@ -37,5 +37,5 @@ docker compose -f docker-compose.dev.yml up

#### Supported Services

- MongoDB
- Redis
- MongoDB ([mongoose](https://mongoosejs.com/))
- Redis ([BullMQ](https://bullmq.io/) and [bull-board](https://github.com/felixmosh/bull-board))
6 changes: 6 additions & 0 deletions __test__/app.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import request from "supertest";
import app from "../src/app";
import { emailQueue, imageProcessingQueue } from "../src/queues";

describe("GET /", () => {
afterAll(async () => {
await emailQueue.close();
await imageProcessingQueue.close();
});

it("should respond with 'Express + TypeScript Server'", async () => {
const response = await request(app).get("/");
expect(response.status).toBe(200);
Expand Down
92 changes: 92 additions & 0 deletions __test__/controllers/jobs/createJob.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { type Request, type Response } from "express";
import { type Queue } from "bullmq";
import { queueByName } from "../../../src/queues";
import createJob from "../../../src/controllers/jobs/createJob.controller";

console.error = jest.fn();

// Mock the queueByName function
jest.mock("../../../src/queues", () => ({
queueByName: jest.fn(),
}));

// Mock the queue's add method
const addMock = jest.fn();

// Create a mock queue object
const queueMock = {
add: addMock,
} as unknown as Queue;

describe("createJob", () => {
let req: Request;
let res: Response;

beforeEach(() => {
req = {
body: {
type: "email",
data: { key: "value" },
},
} as Request;
res = {
json: jest.fn(),
status: jest.fn().mockReturnThis(),
} as unknown as Response;
});

afterEach(() => {
jest.clearAllMocks();
});

it("should add the job to the appropriate queue and return the jobId", async () => {
// Mock the queueByName function to return the queue
(queueByName as jest.Mock).mockReturnValueOnce(queueMock);

// Mock the queue's add method to return a job
const jobId = "job123";
const job = { id: jobId };
addMock.mockResolvedValueOnce(job);

await createJob(req, res);

expect(queueByName).toHaveBeenCalledWith("email");
expect(addMock).toHaveBeenCalledWith("email", { key: "value" });
expect(res.json).toHaveBeenCalledWith({ jobId });
expect(res.status).not.toHaveBeenCalled();
});

it("should handle error when queueByName throws an error", async () => {
const error = new Error("No Queue called unknownQueue");

// Mock the queueByName function to throw an error
(queueByName as jest.Mock).mockImplementationOnce(() => {
throw error;
});

await createJob(req, res);

expect(queueByName).toHaveBeenCalledWith("email");
expect(addMock).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error });
expect(console.error).toHaveBeenCalled();
});

it("should handle error when the queue's add method throws an error", async () => {
const error = new Error("Failed to add job");
addMock.mockRejectedValueOnce(error);

// Mock the queueByName function to return the queue
(queueByName as jest.Mock).mockReturnValueOnce(queueMock);

await createJob(req, res);

expect(queueByName).toHaveBeenCalledWith("email");
expect(addMock).toHaveBeenCalledWith("email", { key: "value" });
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({ error: "Failed to create job" });
expect(console.error).toHaveBeenCalledWith("Error creating job:", error);
});
});
112 changes: 112 additions & 0 deletions __test__/controllers/jobs/getJob.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { type Request, type Response } from "express";
import { Job, type Queue } from "bullmq";
import { queueByName } from "../../../src/queues";
import getJob from "../../../src/controllers/jobs/getJob.controller";

// Mock the queueByName function
jest.mock("../../../src/queues", () => ({
queueByName: jest.fn(),
}));

// Mock the Job class
jest.mock("bullmq", () => ({
Job: {
fromId: jest.fn(),
},
}));

describe("getJob", () => {
let req: Request;
let res: Response;

beforeEach(() => {
req = {
params: {},
} as Request;
res = {
json: jest.fn(),
status: jest.fn().mockReturnThis(),
} as unknown as Response;
});

afterEach(() => {
jest.clearAllMocks();
});

it("should return job details if job exists", async () => {
const jobId = "job123";
const queue = {} as Queue;
const job = {
id: jobId,
name: "Test Job",
getState: jest.fn().mockResolvedValue("completed"),
progress: 100,
data: { key: "value" },
} as unknown as Job;

// Mock the queueByName function to return the queue
(queueByName as jest.Mock).mockReturnValueOnce(queue);

// Mock the Job.fromId function to return the job
(Job.fromId as jest.Mock).mockResolvedValueOnce(job);

await getJob(req, res);

expect(res.json).toHaveBeenCalledWith({
id: job.id,
name: job.name,
status: "completed",
progress: job.progress,
data: job.data,
});
expect(res.status).not.toHaveBeenCalled();
});

it("should return 404 error if job does not exist", async () => {
const queue = {} as Queue;

// Mock the queueByName function to return the queue
(queueByName as jest.Mock).mockReturnValueOnce(queue);

// Mock the Job.fromId function to return null
(Job.fromId as jest.Mock).mockResolvedValueOnce(null);

await getJob(req, res);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: "Job not found" });
});

it("should handle error when queueByName throws an error", async () => {
const error = new Error("No Queue called unknownQueue");

// Mock the queueByName function to throw an error
(queueByName as jest.Mock).mockImplementationOnce(() => {
throw error;
});

await getJob(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error });
});

it("should handle error when Job.fromId throws an error", async () => {
const queue = {} as Queue;
const error = new Error("Failed to fetch job");

// Mock the queueByName function to return the queue
(queueByName as jest.Mock).mockReturnValueOnce(queue);

// Mock the Job.fromId function to throw an error
(Job.fromId as jest.Mock).mockRejectedValueOnce(error);

await getJob(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
error: "Failed to get job status",
});
});
});
40 changes: 40 additions & 0 deletions __test__/jobs/email.job.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { type Job } from "bullmq";
import emailJob from "../../src/jobs/email.job";

describe("emailJob", () => {
let consoleLogSpy: jest.SpyInstance;
let consoleErrorSpy: jest.SpyInstance;

beforeEach(() => {
consoleLogSpy = jest.spyOn(console, "log");
consoleErrorSpy = jest.spyOn(console, "error");
});

afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
});

it("should process email job successfully", async () => {
const jobData = {
to: "[email protected]",
subject: "Test Subject",
body: "Test Body",
};

const job: Partial<Job> = {
id: "jobId",
data: jobData,
name: "emailJob",
};

await emailJob(job as Job);

expect(consoleLogSpy).toHaveBeenCalledWith(
`Sending email to ${jobData.to}: ${jobData.subject}`
);
expect(consoleLogSpy).toHaveBeenCalledWith("Body:", jobData.body);
expect(consoleLogSpy).toHaveBeenCalledWith("Email sent successfully");
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
17 changes: 17 additions & 0 deletions __test__/jobs/imageProcessing.job.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type Job } from "bullmq";
import imageProcessingJob from "../../src/jobs/imageProcessing.job";

describe("imageProcessingJob", () => {
it("should process the image successfully", async () => {
const imageUrl = "https://example.com/image.jpg";
const jobData = { imageUrl };
const job: Partial<Job> = {
id: "job-id",
name: "image-processing",
data: jobData,
// Mock other attributes as needed
};

await expect(imageProcessingJob(job as Job)).resolves.toBeUndefined();
});
});
24 changes: 24 additions & 0 deletions __test__/queues/queueByName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import emailQueue from "../../src/queues/email.queue";
import imageProcessingQueue from "../../src/queues/imageProcessing.queue";
import queueByName from "../../src/queues/queueByName";

describe("queueByName", () => {
afterAll(async () => {
await emailQueue.close();
await imageProcessingQueue.close();
});

it("should return emailQueue for name 'email'", () => {
expect(queueByName("email")).toBe(emailQueue);
});

it("should return imageProcessingQueue for name 'imageProcessing'", () => {
expect(queueByName("imageProcessing")).toBe(imageProcessingQueue);
});

it("should throw an error for unknown queue name", () => {
const unknownName = "unknownQueue";
const expectedErrorMessage = `No Queue called ${unknownName}`;
expect(() => queueByName(unknownName)).toThrowError(expectedErrorMessage);
});
});
6 changes: 6 additions & 0 deletions __test__/routes/posts.route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import app from "../../src/app";
import mongoose from "mongoose";
import Post from "../../src/models/post.model";
import { env } from "../../src/config";
import { emailQueue, imageProcessingQueue } from "../../src/queues";

beforeEach(async () => {
mongoose.set("strictQuery", true);
Expand All @@ -15,6 +16,11 @@ afterEach(async () => {
});

describe("/posts routes", () => {
afterAll(async () => {
await emailQueue.close();
await imageProcessingQueue.close();
});

test("GET /posts", async () => {
const post = await Post.create({
title: "Post 1",
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ services:
- MONGODB_PORT=27017
- MONGODB_USER=root
- MONGODB_PASS=pass
- REDIS_QUEUE_HOST=localhost
- REDIS_QUEUE_HOST=redis
- REDIS_QUEUE_PORT=6379
volumes:
- ./coverage:/project/coverage
Expand Down
Loading

0 comments on commit 02b3e64

Please sign in to comment.