Skip to content

Commit

Permalink
feat: contact create
Browse files Browse the repository at this point in the history
  • Loading branch information
n9mi committed Sep 13, 2024
1 parent e95068d commit 95235dc
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 17 deletions.
126 changes: 126 additions & 0 deletions __tests__/contact.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import prisma from "../src/application/database";
import bcrypt from "bcrypt";
import supertest from "supertest";
import { basePath, web } from "../src/application/web";
import logger from "../src/application/logger";

describe("POST /contact", () => {
let token: string = "";

beforeAll(async () => {
token = await ContactTestUtil.getToken();
});

afterAll(async () => {
await ContactTestUtil.deleteContact();
await ContactTestUtil.deleteUser();
})

const newContact = {
first_name: "First",
last_name: "Last",
email: "[email protected]",
phone: "08123456789"
}

it("should return 200 - success creating contact", async () => {
const res = await supertest(web)
.post(`${basePath}/contact`)
.send(newContact)
.set('Authorization', `Bearer ${token}`);

logger.info(res.body);
expect(res.status).toBe(200);
expect(res.body.data.id).toBeGreaterThanOrEqual(1);
expect(res.body.data.first_name).toBe(newContact.first_name);
expect(res.body.data.last_name).toBe(newContact.last_name);
expect(res.body.data.email).toBe(newContact.email);
expect(res.body.data.phone).toBe(newContact.phone);
});

it("should return 200 - success creating contact with emty last_name, email, and phone", async () => {
const res = await supertest(web)
.post(`${basePath}/contact`)
.send({
first_name: newContact.first_name
})
.set('Authorization', `Bearer ${token}`);

logger.info(res.body);
expect(res.status).toBe(200);
expect(res.body.data.id).toBeGreaterThanOrEqual(1);
expect(res.body.data.first_name).toBe(newContact.first_name);
expect(res.body.data.last_name).toBe("");
expect(res.body.data.email).toBe("");
expect(res.body.data.phone).toBe("");
});

it("should return 400 - bad request invalid email", async () => {
const res = await supertest(web)
.post(`${basePath}/contact`)
.send({
first_name: newContact.first_name,
last_name: newContact.last_name,
email: "test",
})
.set('Authorization', `Bearer ${token}`);

logger.info(res.body);
expect(res.status).toBe(400);
expect(res.body.errors.email).toBeDefined();
});

it("should return 400 - bad request phone number more than 20 characters", async () => {
const res = await supertest(web)
.post(`${basePath}/contact`)
.send({
first_name: newContact.first_name,
last_name: newContact.last_name,
phone: "0123456789012345678901",
})
.set('Authorization', `Bearer ${token}`);

logger.info(res.body);
expect(res.status).toBe(400);
expect(res.body.errors.phone).toBeDefined();
});
})

class ContactTestUtil {
static user = {
name: "user_test_contact",
username: "user_test_contact",
password: "password"
};

static async create() {
await prisma.user.create({
data: {
name: ContactTestUtil.user.name,
username: ContactTestUtil.user.username,
password: await bcrypt.hash(ContactTestUtil.user.password, 10)
}
});
}

static async getToken() {
await ContactTestUtil.create();

const loginRes = await supertest(web)
.post(`${basePath}/auth/login`)
.send({
username: ContactTestUtil.user.username,
password: ContactTestUtil.user.password,
});

return loginRes.body.data.token;
}

static async deleteUser() {
await prisma.user.deleteMany({});
}

static async deleteContact() {
await prisma.contact.deleteMany({});
}
}
34 changes: 17 additions & 17 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@ services:
- POSTGRES_PASSWORD=${DB_PASSWORD}
networks:
- contact_api_network
contact-api:
environment:
- DATABASE_URL=${DATABASE_URL}
- PORT=${PORT}
- BASE_URL_PATH=${BASE_URL_PATH}
- JWT_SECRET=${JWT_SECRET}
- JWT_EXPIRED_IN_MINUTES=${JWT_EXPIRED_IN_MINUTES}
build: .
image: contact-api
ports:
- '3000:3000'
depends_on:
- db
volumes:
- .:/usr/src/node-app
networks:
- contact_api_network
# contact-api:
# environment:
# - DATABASE_URL=${DATABASE_URL}
# - PORT=${PORT}
# - BASE_URL_PATH=${BASE_URL_PATH}
# - JWT_SECRET=${JWT_SECRET}
# - JWT_EXPIRED_IN_MINUTES=${JWT_EXPIRED_IN_MINUTES}
# build: .
# image: contact-api
# ports:
# - '3000:3000'
# depends_on:
# - db
# volumes:
# - .:/usr/src/node-app
# networks:
# - contact_api_network

volumes:
postgres-db:
Expand Down
2 changes: 2 additions & 0 deletions src/application/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getAuthRouter } from "../router/auth";
import { getErrorMiddleware } from "../middleware/error";
import dotenv from "dotenv";
import { getUserRouter } from "../router/user";
import { getContactRouter } from "../router/contact";

dotenv.config();
export const basePath = process.env.BASE_URL_PATH === undefined ? "/api/v1" : process.env.BASE_URL_PATH;
Expand All @@ -11,4 +12,5 @@ export const web = express();
web.use(express.json());
web.use(getAuthRouter(basePath));
web.use(getUserRouter(basePath));
web.use(getContactRouter(basePath));
web.use(getErrorMiddleware());
19 changes: 19 additions & 0 deletions src/controller/contact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Request, Response, NextFunction } from "express";
import ContactService from "../service/contact";
import { ContactRequest } from "../model/contact";

export class ContactController {

static async create(req: Request, res: Response, next: NextFunction) {
try {
const createRes = await ContactService.create(res.locals.user.username, req.body as ContactRequest);

res.status(200)
.json({
data: createRes
});
} catch (e) {
next(e);
}
}
}
26 changes: 26 additions & 0 deletions src/model/contact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Contact } from "@prisma/client"

export interface ContactRequest {
first_name: string,
last_name: string,
email: string,
phone: string
}

export interface ContactResponse {
id: number,
first_name: string,
last_name: string,
email: string,
phone: string
}

export function toContactResponse(contact: Contact): ContactResponse {
return {
id: contact.id,
first_name: contact.first_name,
last_name: contact.last_name ?? "",
email: contact.email ?? "",
phone: contact.phone ?? ""
}
}
11 changes: 11 additions & 0 deletions src/router/contact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import express from "express";
import { ContactController } from "../controller/contact";
import { accessValidation } from "../middleware/accessValidation";

export const getContactRouter = (basePath: string) => {
const contactRouter = express.Router();
contactRouter.use(accessValidation);
contactRouter.post(`${basePath}/contact`, ContactController.create);

return contactRouter;
}
22 changes: 22 additions & 0 deletions src/service/contact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import prisma from "../application/database";
import { ContactRequest, ContactResponse, toContactResponse } from "../model/contact";
import { ContactValidation } from "../validation/contact";
import { Validation } from "../validation/validation";

export default class ContactService {

static async create(username: string, req: ContactRequest): Promise<ContactResponse> {
const validatedReq = Validation.validate(ContactValidation.CREATE, req);

console.info(validatedReq);

const contact = await prisma.contact.create({
data: {
...validatedReq,
username
}
});

return toContactResponse(contact);
}
}
37 changes: 37 additions & 0 deletions src/validation/contact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z, ZodType } from "zod";

export class ContactValidation {
static readonly CREATE: ZodType = z.object({
first_name: z.string({
required_error: "first name is required",
invalid_type_error: "first name should be string"
}).min(1, {
message: "first name length should more than 1 character"
}).max(100, {
message: "first name length should less than 100 character"
}),
last_name: z.string({
invalid_type_error: "last name should be string"
}).min(1, {
message: "last name length should more than 1 character"
}).max(100, {
message: "last name length should less than 100 character"
}).optional(),
email: z.string({
invalid_type_error: "email should be string"
}).email({
message: "email should be valid"
}).min(1, {
message: "email length should more than 1 character"
}).max(100, {
message: "email length should less than 100 character"
}).optional(),
phone: z.string({
invalid_type_error: "phone should be string"
}).min(1, {
message: "phone length should more than 1 character"
}).max(20, {
message: "phone length should less than 20 character"
}).optional(),
})
}

0 comments on commit 95235dc

Please sign in to comment.