Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auth handlers as router #43

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 59 additions & 57 deletions apps/api/__tests__/lib/s3.test.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,60 @@
import { getOne, uploadOne, deleteOne } from "../../lib/s3";

test("getOnePresigned does not return null", async () => {
let url = null;
url = await getOne(
"dev/profile-images/users/1877385915/1-intro-photo-final.jpg"
);
console.log(url);

expect(url).not.toBe(null);
});

test("getting non-existent s3 object throws error", async () => {
await expect(
getOne("dev/code-modules/users/001/001/somenonexistentobject.tsx")
).rejects.toThrow();
});

test("upload test.txt module to s3", async () => {
const url = "dev/code-modules/users/001/004/test.txt";
await uploadOne(url, Buffer.from("Hello world!"));

// Try getting the newly added data
let textData = null;
try {
const presigned = await getOne(url);
const response = await fetch(presigned);

textData = await response.text();
} catch (error) {
console.log(`Something went wrong: ${error}`);
}

expect(textData).toBe("Hello world!");
});

test("deleting s3 objects", async () => {
// Upload new test module
const url = "dev/code-modules/users/001/005/test.txt";
await uploadOne(url, Buffer.from("Hello world!"));

// Try getting the newly added data
let textData = null;
try {
const presigned = await getOne(url);
const response = await fetch(presigned);

textData = await response.text();
} catch (error) {
console.log(`Something went wrong: ${error}`);
}

// Delete newly added data
await deleteOne(url);

// Expect that the deleted url no longer exists
await expect(getOne(url)).rejects.toThrow();
import { getOne, uploadOne, deleteOne } from "../../src/lib/s3";

describe("S3 Module", () => {
it("returns a valid presigned URL for an existing S3 object", async () => {
let url = null;
url = await getOne(
"dev/profile-images/users/1877385915/1-intro-photo-final.jpg"
);
console.log(url);

expect(url).not.toBe(null);
});

it("throws an error when trying to get a non-existent S3 object", async () => {
await expect(
getOne("dev/code-modules/users/001/001/somenonexistentobject.tsx")
).rejects.toThrow();
});

it("successfully uploads and retrieves text data from S3", async () => {
const url = "dev/code-modules/users/001/004/test.txt";
await uploadOne(url, Buffer.from("Hello world!"));

// Try getting the newly added data
let textData = null;
try {
const presigned = await getOne(url);
const response = await fetch(presigned);

textData = await response.text();
} catch (error) {
console.log(`Something went wrong: ${error}`);
}

expect(textData).toBe("Hello world!");
});

it("successfully deletes an S3 object and ensures it no longer exists", async () => {
// Upload new test module
const url = "dev/code-modules/users/001/005/test.txt";
await uploadOne(url, Buffer.from("Hello world!"));

// Try getting the newly added data
let textData = null;
try {
const presigned = await getOne(url);
const response = await fetch(presigned);

textData = await response.text();
} catch (error) {
console.log(`Something went wrong: ${error}`);
}

// Delete newly added data
await deleteOne(url);

// Expect that the deleted url no longer exists
await expect(getOne(url)).rejects.toThrow();
});
});
183 changes: 183 additions & 0 deletions apps/api/__tests__/routes/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import request from "supertest";
import { app } from "../../src/app";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

beforeAll(async () => {
await prisma.$connect();
});

afterAll(async () => {
await prisma.$disconnect();
});

jest.mock("@prisma/client", () => {
const mPrismaClient = {
user: {
findUnique: jest.fn(),
findFirst: jest.fn(),
create: jest.fn(),
},
$connect: jest.fn(),
$disconnect: jest.fn(),
};
return {
PrismaClient: jest.fn(() => mPrismaClient),
};
});

jest.mock("bcrypt", () => ({
compare: jest.fn(),
genSalt: jest.fn(),
hash: jest.fn(),
}));

jest.mock("jsonwebtoken", () => ({
sign: jest.fn(),
}));

describe("POST /api/auth/login", () => {
afterEach(() => {
jest.clearAllMocks();
});

it("should return 400 if username or password is missing", async () => {
const response = await request(app).post("/api/auth/login").send({});
expect(response.status).toBe(400);
expect(response.body.error).toBe("Username and password are required");
});

it("should return 401 if the user is not found", async () => {
(prisma.user.findUnique as jest.Mock).mockResolvedValue(null);

const response = await request(app)
.post("/api/auth/login")
.send({ username: "testuser", password: "password123" });

expect(response.status).toBe(401);
expect(response.body.error).toBe("Invalid credentials");
});

it("should return 401 if the password is invalid", async () => {
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
id: 1,
username: "testuser",
password: "hashedPassword",
});

(bcrypt.compare as jest.Mock).mockResolvedValue(false);

const response = await request(app)
.post("/api/auth/login")
.send({ username: "testuser", password: "password123" });

expect(response.status).toBe(401);
expect(response.body.error).toBe("Invalid credentials");
});

it("should return 200 and set a token if the login is successful", async () => {
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
id: 1,
username: "testuser",
password: "hashedPassword",
});

(bcrypt.compare as jest.Mock).mockResolvedValue(true);
(jwt.sign as jest.Mock).mockReturnValue("mockToken");

const response = await request(app)
.post("/api/auth/login")
.send({ username: "testuser", password: "password123" });

expect(response.status).toBe(200);
expect(response.body.message).toBe("Login successful");
expect(response.headers["set-cookie"]).toBeDefined();
});
});

describe("POST /api/auth/signup", () => {
afterEach(() => {
jest.clearAllMocks();
});

it("should return 400 if required fields are missing", async () => {
const response = await request(app).post("/api/auth/signup").send({});
expect(response.status).toBe(400);
expect(response.body.error).toBe("All fields are required");
});

it("should return 400 if email is already in use", async () => {
(prisma.user.findFirst as jest.Mock).mockResolvedValue({
email: "[email protected]",
});

const response = await request(app).post("/api/auth/signup").send({
email: "[email protected]",
username: "testuser",
password: "password123",
});

expect(response.status).toBe(400);
expect(response.body.error).toBe("Email already in use");
});

it("should return 400 if username is already taken", async () => {
(prisma.user.findFirst as jest.Mock).mockResolvedValue({
username: "testuser",
});

const response = await request(app).post("/api/auth/signup").send({
email: "[email protected]",
username: "testuser",
password: "password123",
});

expect(response.status).toBe(400);
expect(response.body.error).toBe("Username already taken");
});

it("should return 201 and create a new user if data is valid", async () => {
(prisma.user.findFirst as jest.Mock).mockResolvedValue(null);
(bcrypt.genSalt as jest.Mock).mockResolvedValue("mockSalt");
(bcrypt.hash as jest.Mock).mockResolvedValue("mockHashedPassword");
(prisma.user.create as jest.Mock).mockResolvedValue({
id: 1,
email: "[email protected]",
username: "newuser",
password: "mockHashedPassword",
salt: "mockSalt",
});

const response = await request(app).post("/api/auth/signup").send({
email: "[email protected]",
username: "newuser",
password: "password123",
});

expect(response.status).toBe(201);
expect(response.body.message).toBe("User created successfully");
expect(response.body.user).toEqual({
id: 1,
email: "[email protected]",
username: "newuser",
});
});

it("should return 500 if there is a server error", async () => {
(prisma.user.findFirst as jest.Mock).mockImplementation(() => {
throw new Error("Database error");
});

const response = await request(app).post("/api/auth/signup").send({
email: "[email protected]",
username: "newuser",
password: "password123",
});

expect(response.status).toBe(500);
expect(response.body.error).toBe("Internal server error");
});
});
11 changes: 6 additions & 5 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
"start": "node dist/index.js",
"build": "tsc",
"dev": "nodemon src/index.ts",
"test": "jest --coverage=true"
"test": "jest --coverage --detectOpenHandles"
},
"dependencies": {

"@aws-sdk/client-s3": "^3.717.0",
"@aws-sdk/s3-request-presigner": "^3.717.0",
"@prisma/client": "^5.10.2",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.8",
Expand All @@ -20,17 +21,17 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"jsonwebtoken": "^9.0.2",
"@aws-sdk/client-s3": "^3.717.0",
"@aws-sdk/s3-request-presigner": "^3.717.0"
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^20",
"@types/supertest": "^6.0.2",
"jest": "^29.7.0",
"nodemon": "^3.1.0",
"prisma": "^5.10.2",
"supertest": "^7.0.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
Expand Down
19 changes: 19 additions & 0 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import express from "express";
import cors from "cors";

import dotenv from "dotenv";
import cookieParser from "cookie-parser";

import { authRouter } from "./routes";

dotenv.config();

const app = express();

app.use(cors());
app.use(express.json());
app.use(cookieParser());

app.use("/api/auth", authRouter);

export { app };
Loading
Loading