CRUD & Test server
文章目录 CRUD & Test serverTicketing ServiceProject SetupRunning the Ticket ServiceMongo Connection URIAuth 服务同样修改 MONGO_URI 配置先写 test 再写业务代码的习惯创建 Router增加单个微服务的 Auth 认证机制在测试期间伪造身份验证Building a Session测试无效请求Title 和 Price 的验证用 TypeScript 对 Mongoose 进行约束定义 Ticket Model在 Route Handler 的进行数据库操作测试 Show Tickets 的 Routes不可预料的错误What's that Error?!更好的 Error Logging 以便于测试index 路由 和 测试Ticket UpdatingHandling Updates权限判断最后 Update 的 Code Ticketing Service
创建服务的步骤
Create package.json, install depsWrite DockerfileCreate index.ts to run projectBuild image, push to docker hubWrite k8s file for deployment, serviceUpdate skaffold.yaml to do file sync for ticketsWrite k8s file for Mongodb deployment, service为了节约时间,直接从 auth 服务里 copy 即可,然后
Create package.json, install depsWrite DockerfileCreate index.ts to run project? back to top
Project Setup Build image, push to docker hub docker build -t heysirius/tickets . docker push heysirius/tickets? back to top
Running the Ticket Service Write k8s file for deployment, serviceUpdate skaffold.yaml to do file sync for ticketsWrite k8s file for Mongodb deployment, service kubectl get pods cd ticketing skaffold dev如果已经在 skaffold 下了,其实可以直接跳过这两个 Step,因为自动化部署 ? back to top
Mongo Connection URI和 JWT 一样,我们直接在 container 的环境中,配置 MONGO_URI
# infra/k8s/tickets-mongo-depl.yaml apiVersion: apps/v1 kind: Deployment metadata: name: tickets-depl spec: replicas: 1 selector: matchLabels: app: tickets template: metadata: labels: app: tickets spec: containers: - name: tickets image: heysirius/tickets env: - name: MONGO_URI value: 'mongodb://tickets-mongo-srv:27017/tickets' - name: JWT_KEY valueFrom: secretKeyRef: name: jwt-secret key: JWT_KEY --- ... // index.ts try { await mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true }); console.log('Connected to MongoDb'); } catch (err) { console.log(err); }? back to top
Auth 服务同样修改 MONGO_URI 配置 - 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); }? back to top
先写 test 再写业务代码的习惯 // routes/__test__/new.test.ts 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 () => {});? back to top
创建 Router // routes/__test__/new.test.ts 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); }); // routes/new.ts 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.ts app.use(createTicketRouter);? back to top
增加单个微服务的 Auth 认证机制 // routes/__test__/new.test.ts it('can only be accessed if the user is signed in', async () => { await request(app).post('/api/tickets').send({}).expect(401); }); // app.ts app.use(currentUser); 引入 Auth 中间件 // routes/new.ts import express, { Request, Response } from 'express'; import { requireAuth } from '@heysirius-common/common'; const router = express.Router(); router.post('/api/tickets', requireAuth, (req: Request, res: Response) => { res.sendStatus(200); }); export { router as createTicketRouter };? back to top
在测试期间伪造身份验证 // routes/__test__/new.test.ts 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==
https://·" } // 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}`]; };
? back to top
测试无效请求 这里要无效,是 price 和 title // routes/__test__/new.test.ts 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); });? back to top
Title 和 Price 的验证 回顾下我们有的 middleware // routes/new.ts import express, { Request, Response } from 'express'; import { body } from 'express-validator'; import { requireAuth, validateRequest } from '@heysirius-common/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 };? back to top
用 TypeScript 对 Mongoose 进行约束 // models/ticket.ts 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; }? back to top
定义 Ticket Model 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 };? back to top
在 Route Handler 的进行数据库操作 // routes/__test__/new.test.ts 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); }); // routes/new.ts import express, { Request, Response } from 'express'; import { body } from 'express-validator'; import { requireAuth, validateRequest } from '@heysirius-common/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 };? back to top
测试 Show Tickets 的 Routes // routes/show.ts 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); });? back to top
不可预料的错误 // routes/show.ts import express, { Request, Response } from 'express'; import { NotFoundError } from '@heysirius-common/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);? back to top
What’s that Error?! 直接在 node_module/heysirius-common/build/middlewares/error-handler 新增 console.log(error)这样就能跳过 npm publish 的环节,毕竟是调试而不是正式发布 修复 // routes/__test__/show.test.ts 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); });? back to top
更好的 Error Logging 以便于测试 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' }] }); };? back to top
index 路由 和 测试 // routes/__test__/index.test.ts 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); }); // routes/index.ts 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.ts app.use(indexTicketRouter);? back to top
Ticket Updating // routes/__test__/update.test.ts 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); });? back to top
Handling Updates // routes/update.ts import express, { Request, Response } from 'express'; import { body } from 'express-validator'; import { validateRequest, NotFoundError, requireAuth, NotAuthorizedError, } from '@heysirius-common/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 };? back to top
权限判断 如果该用户没有拥有 ticket,就不能 update // routes/__test__/update.test.ts 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); }); // routes/update.ts import express, { Request, Response } from 'express'; import { body } from 'express-validator'; import { validateRequest, NotFoundError, requireAuth, NotAuthorizedError, } from '@heysirius-common/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" }? back to top
最后 Update 的 Code // routes/__test__/update.test.ts 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); }); // routes/update.ts import express, { Request, Response } from 'express'; import { body } from 'express-validator'; import { validateRequest, NotFoundError, requireAuth, NotAuthorizedError, } from '@heysirius/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 };? back to top
1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。 |