irpas技术客

【深入浅出 Node + React 的微服务项目】13. CRUD & Test server_嗨Sirius

网络投稿 901

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.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #深入浅出 #node #React #的微服务项目13 #CRUD #amp #test