- Section 13: Create-Read-Update-Destroy Server Setup
- Table of Contents
- Ticketing Service Overview
- Project Setup
- Running the Ticket Service
- Mongo Connection URI
- Quick Auth Update
- Test-First Approach
- Creating the Router
- Adding Auth Protection
- Faking Authentication During Tests
- Building a Session
- Testing Request Validation
- Validating Title and Price
- Reminder on Mongoose with TypeScript
- Defining the Ticket Model
- Creation via Route Handler
- Testing Show Routes
- Unexpected Failure!
- What's that Error?!
- Better Error Logging
- Complete Index Route Implementation
- Ticket Updating
- Handling Updates
- Permission Checking
- Final Update Changes
- Manual Testing
Steps
- Create package.json, install deps
- Write Dockerfile
- Create index.ts to run project
- Build image, push to docker hub
- Write k8s file for deployment, service
- Update skaffold.yaml to do file sync for tickets
- Write k8s file for Mongodb deployment, service
Copy from auth service to save time!
- Create package.json, install deps
- Write Dockerfile
- Create index.ts to run project
- Build image, push to docker hub
docker build -t chesterheng/tickets .
docker push chesterheng/tickets
- Write k8s file for deployment, service
- Update skaffold.yaml to do file sync for tickets
- Write k8s file for Mongodb deployment, service
kubectl get pods
cd section-13/ticketing
skaffold dev
- name: MONGO_URI
value: 'mongodb://tickets-mongo-srv:27017/tickets'
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
});
console.log('Connected to MongoDb');
} catch (err) {
console.log(err);
}
- name: MONGO_URI
value: 'mongodb://auth-mongo-srv:27017/auth'
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
});
console.log('Connected to MongoDb');
} catch (err) {
console.log(err);
}
import request from 'supertest';
import { app } from '../../app';
it('has a route handler listening to /api/tickets for post requests', async () => {});
it('can only be accessed if the user is signed in', async () => {});
it('returns an error if an invalid title is provided', async () => {});
it('returns an error if an invalid price is provided', async () => {});
it('creates a ticket with valid inputs', async () => {});
it('has a route handler listening to /api/tickets for post requests', async () => {
const response = await request(app)
.post('/api/tickets')
.send({});
expect(response.status).not.toEqual(404);
});
import express, { Request, Response } from 'express';
const router = express.Router();
router.post('/api/tickets', (req: Request, res: Response) => {
res.sendStatus(200);
});
export { router as createTicketRouter };
app.use(createTicketRouter);
it('can only be accessed if the user is signed in', async () => {
await request(app).post('/api/tickets').send({}).expect(401);
});
app.use(currentUser);
import express, { Request, Response } from 'express';
import { requireAuth } from '@chticketing/common';
const router = express.Router();
router.post('/api/tickets', requireAuth, (req: Request, res: Response) => {
res.sendStatus(200);
});
export { router as createTicketRouter };
it('returns a status other than 401 if the user is signed in', async () => {
const response = await request(app).post('/api/tickets').send({});
expect(response.status).not.toEqual(401);
});
cookie: express:sess=eyJqd3QiOiJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKcFpDSTZJalZtTVRRd016Y3lPRFUyWkdRek1EQXhPV1U1TkdFd1pTSXNJbVZ0WVdsc0lqb2lkR1Z6ZEVCMFpYTjBMbU52YlNJc0ltbGhkQ0k2TVRVNU5URTBOekV5TW4wLkVicVlVVmY5SjIyUjlOa3k5dVhKdHl3WEh2MVI4ZURuQUlSWFl3RWw4UkEifQ==
- Build a JWT payload. { id, email }
- Create the JWT!
- Build Session object. { jwt: MY_JWT }
- Turn that session into JSON
- Take JSON and encode it as base64
- return a string thats the cookie with encoded data
it('returns a status other than 401 if the user is signed in', async () => {
const response = await request(app)
.post('/api/tickets')
.set('Cookie', global.signin())
.send({});
expect(response.status).not.toEqual(401);
});
global.signin = () => {
// Build a JWT payload. { id, email }
const payload = {
id: "5f140372856dd30019e94a0e",
email: "test@test.com"
}
// Create the JWT!
const token = jwt.sign(payload, process.env.JWT_KEY!);
// Build Session object. { jwt: MY_JWT }
const session = { jwt: token }
// Turn that session into JSON
const sessionJSON = JSON.stringify(session);
// Take JSON and encode it as base64
const base64 = Buffer.from(sessionJSON).toString('base64');
// return a string thats the cookie with encoded data
return [`express:sess=${base64}`];
};
it('returns an error if an invalid title is provided', async () => {
await request(app)
.post('/api/tickets')
.set('Cookie', global.signin())
.send({
title: '',
price: 10,
})
.expect(400);
await request(app)
.post('/api/tickets')
.set('Cookie', global.signin())
.send({
price: 10,
})
.expect(400);
});
it('returns an error if an invalid price is provided', async () => {
await request(app)
.post('/api/tickets')
.set('Cookie', global.signin())
.send({
title: 'asldkjf',
price: -10,
})
.expect(400);
await request(app)
.post('/api/tickets')
.set('Cookie', global.signin())
.send({
title: 'laskdfj',
})
.expect(400);
});
import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import { requireAuth, validateRequest } from '@chticketing/common';
const router = express.Router();
router.post(
'/api/tickets',
requireAuth,
[
body('title').not().isEmpty().withMessage('Title is required'),
body('price')
.isFloat({ gt: 0 })
.withMessage('Price must be greater than 0'),
],
validateRequest,
(req: Request, res: Response) => {
res.sendStatus(200);
}
);
export { router as createTicketRouter };
import mongoose from 'mongoose';
interface TicketAttrs {
title: string;
price: number;
userId: string;
}
interface TicketDoc extends mongoose.Document {
title: string;
price: number;
userId: string;
}
interface TicketModel extends mongoose.Model<TicketDoc> {
build(attrs: TicketAttrs): TicketDoc;
}
const ticketSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
userId: {
type: String,
required: true
}
}, {
toJSON: {
transform(doc, ret) {
ret.id = ret._id;
delete ret._id;
}
}
});
ticketSchema.statics.build = (attrs: TicketAttrs) => {
return new Ticket(attrs);
};
const Ticket = mongoose.model<TicketDoc, TicketModel>('Ticket', ticketSchema);
export { Ticket };
it('creates a ticket with valid inputs', async () => {
let tickets = await Ticket.find({});
expect(tickets.length).toEqual(0);
const title = 'asldkfj';
await request(app)
.post('/api/tickets')
.set('Cookie', global.signin())
.send({
title,
price: 20,
})
.expect(201);
tickets = await Ticket.find({});
expect(tickets.length).toEqual(1);
expect(tickets[0].price).toEqual(20);
expect(tickets[0].title).toEqual(title);
});
import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import { requireAuth, validateRequest } from '@chticketing/common';
import { Ticket } from '../models/ticket';
const router = express.Router();
router.post(
'/api/tickets',
requireAuth,
[
body('title').not().isEmpty().withMessage('Title is required'),
body('price')
.isFloat({ gt: 0 })
.withMessage('Price must be greater than 0'),
],
validateRequest,
async (req: Request, res: Response) => {
const { title, price } = req.body;
const ticket = Ticket.build({
title,
price,
userId: req.currentUser!.id
});
await ticket.save();
res.sendStatus(201).send(ticket);
}
);
export { router as createTicketRouter };
it('returns a 404 if the ticket is not found', async () => {
await request(app).get('/api/tickets/laskdjfalksfdlkakj').send().expect(404);
});
it('returns the ticket if the ticket is found', async () => {
const title = 'concert';
const price = 20;
const response = await request(app)
.post('/api/tickets')
.set('Cookie', global.signin())
.send({
title,
price,
})
.expect(201);
const ticketResponse = await request(app)
.get(`/api/tickets/${response.body.id}`)
.send()
.expect(200);
expect(ticketResponse.body.title).toEqual(title);
expect(ticketResponse.body.price).toEqual(price);
});
import express, { Request, Response } from 'express';
import { NotFoundError } from '@chticketing/common';
import { Ticket } from '../models/ticket';
const router = express.Router();
router.get('/api/tickets/:id', async (req: Request, res: Response) => {
const ticket = await Ticket.findById(req.params.id);
if (!ticket) {
throw new NotFoundError();
}
res.send(ticket);
});
export { router as showTicketRouter };
app.use(showTicketRouter);
it('returns a 404 if the ticket is not found', async () => {
const id = new mongoose.Types.ObjectId().toHexString();
await request(app)
.get(`/api/tickets/${id}`)
.send();
console.log(response.body);
});
import { Request, Response, NextFunction } from 'express';
import { CustomError } from '../errors/custom-error';
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
if(err instanceof CustomError) {
return res.status(err.statusCode).send({ errors: err.serializeErrors() });
}
console.error(err);
res.status(400).send({
errors: [{ message: 'Something went wrong' }]
});
};
import request from 'supertest';
import { app } from '../../app';
const createTicket = () => {
return request(app).post('/api/tickets').set('Cookie', global.signin()).send({
title: 'asldkf',
price: 20,
});
};
it('can fetch a list of tickets', async () => {
await createTicket();
await createTicket();
await createTicket();
const response = await request(app).get('/api/tickets').send().expect(200);
expect(response.body.length).toEqual(3);
});
import express, { Request, Response } from 'express';
import { Ticket } from '../models/ticket';
const router = express.Router();
router.get('/api/tickets', async (req: Request, res: Response) => {
const tickets = await Ticket.find({});
res.send(tickets);
});
export { router as indexTicketRouter };
app.use(indexTicketRouter);
it('returns a 404 if the provided id does not exist', async () => {
const id = new mongoose.Types.ObjectId().toHexString();
await request(app)
.put(`/api/tickets/${id}`)
.set('Cookie', global.signin())
.send({
title: 'aslkdfj',
price: 20,
})
.expect(404);
});
it('returns a 401 if the user is not authenticated', async () => {
const id = new mongoose.Types.ObjectId().toHexString();
await request(app)
.put(`/api/tickets/${id}`)
.send({
title: 'aslkdfj',
price: 20,
})
.expect(401);
});
import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import {
validateRequest,
NotFoundError,
requireAuth,
NotAuthorizedError,
} from '@chticketing/common';
import { Ticket } from '../models/ticket';
const router = express.Router();
router.put(
'/api/tickets/:id',
requireAuth,
async (req: Request, res: Response) => {
const ticket = await Ticket.findById(req.params.id);
if (!ticket) {
throw new NotFoundError();
}
res.send(ticket);
}
);
export { router as updateTicketRouter };
it('returns a 401 if the user does not own the ticket', async () => {
const response = await request(app)
.post('/api/tickets')
.set('Cookie', global.signin())
.send({
title: 'asldkfj',
price: 20,
});
await request(app)
.put(`/api/tickets/${response.body.id}`)
.set('Cookie', global.signin())
.send({
title: 'alskdjflskjdf',
price: 1000,
})
.expect(401);
});
import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import {
validateRequest,
NotFoundError,
requireAuth,
NotAuthorizedError,
} from '@chticketing/common';
import { Ticket } from '../models/ticket';
const router = express.Router();
router.put(
'/api/tickets/:id',
requireAuth,
async (req: Request, res: Response) => {
const ticket = await Ticket.findById(req.params.id);
if (!ticket) {
throw new NotFoundError();
}
if (ticket.userId !== req.currentUser!.id) {
throw new NotAuthorizedError();
}
res.send(ticket);
}
);
export { router as updateTicketRouter };
const payload = {
id: new mongoose.Types.ObjectId().toHexString(),
email: "test@test.com"
}
it('returns a 400 if the user provides an invalid title or price', async () => {
const cookie = global.signin();
const response = await request(app)
.post('/api/tickets')
.set('Cookie', cookie)
.send({
title: 'asldkfj',
price: 20,
});
await request(app)
.put(`/api/tickets/${response.body.id}`)
.set('Cookie', cookie)
.send({
title: '',
price: 20,
})
.expect(400);
await request(app)
.put(`/api/tickets/${response.body.id}`)
.set('Cookie', cookie)
.send({
title: 'alskdfjj',
price: -10,
})
.expect(400);
});
it('updates the ticket provided valid inputs', async () => {
const cookie = global.signin();
const response = await request(app)
.post('/api/tickets')
.set('Cookie', cookie)
.send({
title: 'asldkfj',
price: 20,
});
await request(app)
.put(`/api/tickets/${response.body.id}`)
.set('Cookie', cookie)
.send({
title: 'new title',
price: 100,
})
.expect(200);
const ticketResponse = await request(app)
.get(`/api/tickets/${response.body.id}`)
.send();
expect(ticketResponse.body.title).toEqual('new title');
expect(ticketResponse.body.price).toEqual(100);
});
import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import {
validateRequest,
NotFoundError,
requireAuth,
NotAuthorizedError,
} from '@chticketing/common';
import { Ticket } from '../models/ticket';
const router = express.Router();
router.put(
'/api/tickets/:id',
requireAuth,
[
body('title').not().isEmpty().withMessage('Title is required'),
body('price').isFloat({ gt: 0 }).withMessage('Price must be provided and must be greater than 0'),
],
validateRequest,
async (req: Request, res: Response) => {
const ticket = await Ticket.findById(req.params.id);
if (!ticket) {
throw new NotFoundError();
}
if (ticket.userId !== req.currentUser!.id) {
throw new NotAuthorizedError();
}
ticket.set({
title: req.body.title,
price: req.body.price
})
await ticket.save();
res.send(ticket);
}
);
export { router as updateTicketRouter };