木头的博客

我是木头 有些想法 有点精力

0%

第二章我们简单的介绍了下如何连接测试数据库,这篇我们将结合 TDD 来完整的实现注册和登录功能。(如果接下来我写的内容你已经做过,可以跳过该步)

1. 编写第一个集成测试

按照 Nest 提供的示例 E2E 测试, 位于 test/app.e2e-spec.ts 应该长这样

test/app.e2e-spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Test, TestingModule } from '@nestjs/testing'
import * as request from 'supertest'
import { AppModule } from './../src/app.module'

describe('AppController (e2e)', () => {
let app

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile()

app = moduleFixture.createNestApplication()
await app.init()
})

it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!')
})
})

现在是运行不了的,不过没关系,我们稍稍改造一下

首先我们测试一个用于检查健康的接口 /hello, 无需导入整个 AppModule, 只需导入 AppController ,改造为下列的样子

app.e2e-spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Test, TestingModule } from '@nestjs/testing'
import { AppController } from 'app.controller'
import * as request from 'supertest'

describe('app module', () => {
let app

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
controllers: [AppController],
}).compile()

app = moduleFixture.createNestApplication()
await app.init()
})

afterAll(async () => {
await app.close()
})

it('/hello (GET)', () => {
return request(app.getHttpServer())
.get('/hello?name=world')
.expect(200)
.expect('Hello world!')
})
})

然后执行

1
yarn test:e2e

yarn e2e test

阅读全文 »

这一节, 我们引入 Swagger 来自动根据代码里的注脚来生成接口文档。

Nest 为我们提供了一个专用的模块来搭配 Swagger 来使用

1. 安装依赖

1
yarn add @nestjs/swagger swagger-ui-express

2. 初始化 Swagger 模块

在我们的应用入口文件 main.ts 中添加一个 createSwagger 方法, 并在 bootstrap 方法中初始化它

main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { INestApplication, ValidationPipe } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
import { AppModule } from 'app.module'

function createSwagger(app: INestApplication) {
const version = require('../package.json').version || ''

const options = new DocumentBuilder()
.setTitle('Nestjs Realworld Example App')
.setVersion(version)
.addBearerAuth()
.build()

const document = SwaggerModule.createDocument(app, options)
SwaggerModule.setup('/docs', app, document)
}

async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe())

if (process.env.SWAGGER_ENABLE && process.env.SWAGGER_ENABLE === 'true') {
createSwagger(app)
}

await app.listen(3000)
}

bootstrap().catch((err) => console.error(err))

createSwagger 中, 我们首先读取了来自 package.json 中的版本号来作为接口的版本

然后我们设置了 Title 和 Bearer 鉴权认证入口, 我们还设置了 /docs 为我们文档的入口

最后,我们判断环境变量中的 SWAGGER_ENABLE 是否打开, 如果打开我们就初始化 Swagger 文档系统。

.env.env.template 中增加 SWAGGER_ENABLE=true, 然后启动服务器

访问 http://localhost:3000/docs 就能看见我们的接口文档创建好啦!

swagger preview

阅读全文 »

这里记录了一些个人学习 Flutter 时遇到的一些问题, 可以作为避免踩坑和速查手册. 如有疑问欢迎留言.

阅读全文 »

上一章中, 我们完成了鉴权功能, 也就是 Auth 模块, 顺便简单实现了下注册和登录功能. 这一章我们就来正式的将登录和实现功能做完, 含有完整的数据校验和转化.

在正式的开始完善功能之前, 我们现清理重构一下已有的代码.

1. 重构代码

1.1 整理路由

我们上一张的登录和注册是写在 app.controller.ts 中的, 如果所有的路由都写在这里的话就会比较杂乱, 所以我们将它移动到 Auth 模块下.

首先生成 AuthController

1
nest g controller auth

然后将 /auth/register/auth/login 移到 AuthController 下

auth.controller.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { Body, Controller, Post, Request, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
import { UserService } from '../user/user.service'
import { AuthService } from './auth.service'

@Controller('auth')
export class AuthController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthService,
) {}

@Post('/register')
async register(
@Body() requestBody: { email: string; username: string; password: string },
) {
const user = await this.userService.createUser(requestBody)
const token = this.authService.generateToken(user.id, user.username)
return {
user: { ...user, token },
}
}

@UseGuards(AuthGuard('local'))
@Post('/login')
async login(@Request() req) {
const { user } = req
const token = this.authService.generateToken(user.id, user.username)
return {
user: { ...user, token },
}
}
}

顺便也移动下测试代码, 这里就不再赘述了.

阅读全文 »

上一章我们创建了一个用户表, 但是还没有实现真正的注册和登录. 要实现注册登录以及后续的权限校验, 我们还有一些工作要做.

目前有比较多的思路来对用户进行鉴权, 我们选用 Conduit 示例中展示的也是现在比较广泛的做法 JWT 进行认证.

要实现 JWT 鉴权, NestJS 为我们做好了大部分工作.

1. 安装依赖

但是在这之前, 我们要先安装下面的依赖

1
2
yarn add @nestjs/passport passport passport-local
yarn add -D @types/passport-local

Passport 你可以把它看作是一个小型的框架, 因为你可以通过一些简单的回调函数来进行配置. Passport 会在适当的时候对其进行调用.

@nestjs/passport 则对 Passport 进行了很好的集成.

阅读全文 »

上一个文章我们介绍了如何搭建一个开发环境和 Pipeline, 这篇文章开始我们将正式的用 TDD 的模式实现一个后端项目.

1. 安装依赖

我们选用了 Postgres 作为我们的数据库, 操作数据库的 ORM 我们选用 TypeORM, 这是一个 TypeScript 友好的 ORM, 并且 nest 也提供了非常便利的集成方法.

1
yarn add @nestjs/typeorm typeorm pg

2. 接入 TypeORM

编辑 app.module.ts

app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { TypeOrmModule } from '@nestjs/typeorm'

@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'realworld',
password: '123456',
database: 'nestjs',
entities: ['dist/**/*.entity{.ts,.js}'],
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
阅读全文 »

emmm, 最近计划着学习后端,本来想从 Java 开始,奈何新的知识点一股脑涌进来,只知道教程怎么做,而不说为什么,很是迷茫。

于是想从熟悉的技术栈开始,结合最近学习的知识(TDD、Docker、GitHub Actions)和想学的知识(NestJS、Postgres、Swagger)一步一步巩固和学习。

顺便记录下来沉淀和输出自己的知识,也希望能帮到大家少走一些弯路,告别 2019,迎接 2020!

0. 内容预告

我们这次将要实现的系统是 Conduit 的 API 部分,前些时间我已经 TDD 实践实现了 Conduit 的前端部分,技术栈选择了 Preact.

Conduit 是什么,这是一个基于 Realworld 的示例项目。Realworld 集合了现今大部分的前后端框架,他们用不同的语言和技术展实现了同一个系统,也就是我们这次要做的 Conduit。

当然,Realworld 现在也有 NestJS 的实现,不过既然是结合自己的知识点来学,当然不能照抄啦,假装网友们还没用 NestJS 实现它好啦,[偷笑][偷笑]

这次用到的技术栈有:Nestjs TypeScript Postgres Jest Docker Github Actions Swagger ESLint , 然后我们会以 TDD 的方式进行开发,遵循“红-绿-重构”的方式一步一步的完成我们的项目。

好,话不多说,赶紧进入实战演练吧!

阅读全文 »

通信

容器间通信的一些 Q&A

  1. 如果不声明 network 字段, 在同一个 docker compose 之间可以通信吗?

    可以

    如果不指定一个特定的 network, docker 会指定容器在一个名为 docker0 的默认网桥中

  2. 如果不声明 expose 选项, 那么同一网络中的其他容器可以访问该容器吗?

    可以

    但如果启动docker守护进程时指定了--icc=false选项,则不可以。
    建议声明, 声明该选项有助于使用者了解容器内暴露了那些端口出来供使用。

  3. 在同一个网络中不同的容器使用了同一个端口会有冲突吗?

    不会

    不同的容器使用不同的 host, 所以哪怕两个容器使用了同样的端口,也不会与其他容器冲突。

  4. exposeports 字段有什么区别?

    如果容器内的端口需要在宿主环境访问,则需提供 ports 字段

    expose 字段在默认情况下是可选的,即使不声明也可以在同一个网络中使用该容器的端口

最近在使用 Cypress 作为前端项目的 E2E 测试,发布到 CI 环境时自动运行。

运行时发现在 Pipeline 测试报告中有生成 mp4 格式的视频,这才想起来 Cypress 自带生成视频快照的功能,结合 Jenkins 收集报告产物,不就可以拿到视频快照了吗?

哈哈,talk is cheap, show my code!

下面是 jenkins 配置

Jenkinsfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pipeline {
agent any

environment {
CHROME_BIN = '/bin/google-chrome'
}

stages {
stage('Environment') {
steps {
sh 'uname -a'
sh 'apt-get update'
sh 'apt-get install -y xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 ttf-wqy-zenhei'
sh 'fc-cache -v'
sh 'yarn install'
}
}
stage('Test') {
steps {
sh 'yarn test:e2e --headless'
junit 'reporter/output.xml'
archiveArtifacts 'tests/e2e/videos/*.mp4'
}
}
}
}

其中 ttf-wqy-zenhei 是用来解决 Ubuntu 系统中没有中文字体等问题,否则生成的视频报告中的中文都为方框乱码。

如果是在 CentOS 中,执行 yum -y groupinstall chinese-support 解决中文问题

如果不知道 Jenkins 所属运行环境,使用 uname -a 查看系统信息

archiveArtifacts 用来收集报告产物

junit 用来收集测试报告,但 Cypress 默认是不生成报告的,需要在 cypress.json 中增加以下内容

cypress.json
1
2
3
4
5
6
7
{
"reporter": "junit",
"reporterOptions": {
"mochaFile": "reporter/output.xml",
"toConsole": true
}
}
阅读全文 »

我们在写 shell 脚本时经常会遇到一些需要交互的操作,比如修改某个文件,或是使用 yum install ssh-keygen certbot --nginx 等操作时,需要输入一些指令如 “y”, “Enter” 和其他的一些信息。

我们写脚本就是为了自动操作,怎么可以等命令执行一会之后在按个回车进行下一步呢?既然我知道接下来要输入什么命令,我告诉你你帮我输入了不就得了?

聪明tōu lǎn的我们想到了一些办法来避免这种无谓的等待,记录下来分享给大家

阅读全文 »