Skip to content

03. Week 3 Keynotes

Tran Phong Phu edited this page May 24, 2019 · 12 revisions

Week 3 - Keynotes

Working with Gin

A web framework should to have:

Ngoài các yếu tố về performance, simple, security, thì mình đánh giá một framework phải có một cộng đồng hỗ trợ mạnh với hệ sinh thái về plugin và giải pháp đa dạng.

Session Cookie HTTP Request, Method, Body, Querystring, URI(max length)

Todos:

Create an note service to:

type Note struct {
	gorm.Model
	Title     string
	Completed bool
}
  1. Create a note
  2. Find a note
  3. Update a note
  4. Delete a note
  5. List notes

REST API

Do you know about POST/GET?

  • Querystring
  • Request Body
  • Sercurity

See more discussion here: What is the difference between POST and GET?

Create a note

$ curl http://localhost:8081/note -d '{"title":"Todo 1", "completed": false}' | jq .

Output

{
  "ID": 1,
  "CreatedAt": "2019-03-30T00:03:28.200475675+07:00",
  "UpdatedAt": "2019-03-30T00:03:28.200475675+07:00",
  "DeletedAt": null,
  "Title": "",
  "Completed": false
}

Find a note

$ curl http://localhost:8081/note/1 | jq .

Output

{
  "ID": 1,
  "CreatedAt": "2019-03-30T00:03:28.200475675+07:00",
  "UpdatedAt": "2019-03-30T00:03:28.200475675+07:00",
  "DeletedAt": null,
  "Title": "",
  "Completed": false
}

Gin Middleware

func ASimpleMiddleware(c *gin.Context) {
  if true {
		c.AbortWithStatus(400)
		return
	}
	fmt.Println("Print here for every request")
	c.Next()
}
  • Middleware nó là một stack
  • Middleware trước nó có thể cancel/abort những middleware sau đó.
  • Mình có thể sử dụng để tạo ra các giá trị dùng chung cho từng route

Do you exercise: signin/login/jwt authentication

Working Databases

MySQL

You should do this as your exercise

MongoDB

You should do this as your exercise

ElasticSearch

You should do this as your exercise

Testing with K6

$ docker run -i loadimpact/k6 run --vus 2 -d 5s  - <script.js

Test vấn đề Atomic với bài toán counting tăng dần, lệch một lượng so với mong đợi:

    ✗ status was 200
     ↳  0% — ✓ 0 / ✗ 14218
    ✗ transaction time OK
     ↳  99% — ✓ 14132 / ✗ 86

    checks.....................: 49.69% ✓ 14122 ✗ 14294
    data_received..............: 2.0 MB 407 kB/s
    data_sent..................: 1.3 MB 270 kB/s
    http_req_blocked...........: avg=9.08µs  min=950ns    med=3.2µs   max=5.07ms  p(90)=5.23µs  p(95)=7.69µs
    http_req_connecting........: avg=2.69µs  min=0s       med=0s      max=4.58ms  p(90)=0s      p(95)=0s
    http_req_duration..........: avg=2.47ms  min=406.18µs med=2.18ms  max=34.09ms p(90)=4.1ms   p(95)=5ms
    http_req_receiving.........: avg=44.33µs min=4.81µs   med=17.04µs max=20.72ms p(90)=65.36µs p(95)=116.84µs
    http_req_sending...........: avg=43.85µs min=5.11µs   med=16.21µs max=14.21ms p(90)=56.21µs p(95)=98.45µs
    http_req_tls_handshaking...: avg=0s      min=0s       med=0s      max=0s      p(90)=0s      p(95)=0s
    http_req_waiting...........: avg=2.38ms  min=376.17µs med=2.11ms  max=24.75ms p(90)=3.99ms  p(95)=4.84ms
    http_reqs..................: 14210  2841.344056/s
    iteration_duration.........: avg=2.8ms   min=514.38µs med=2.45ms  max=35.14ms p(90)=4.53ms  p(95)=5.58ms
    iterations.................: 14208  2840.944148/s
    vus........................: 10     min=10  max=10
    vus_max....................: 10     min=10  max=10

Từ k6 test ta mong đợi counter sẽ có giá trị là: 14208, nhưng thực tế chạy được chỉ là 14132 - 1, khi chúng ta gọi hàm tiếp theo.

Các bạn tham khảo file k6/script.js và hàm pingHandler

var counter int = 0

func pingHandler(c *gin.Context) {
	counter += 1
	// Race condition => Hai CPU cung access vo 1 cai bien
	// Atomic => co 100 request vao nhung expected: counter = 100, actual: 80
	c.Writer.Header().Set("X-Counter", strconv.Itoa(counter))
	c.String(201, "Pong\n")
}

Exercise

  1. Unit Test/Mock
  2. Validation
  3. Authentication/Identity Context

Unit Test/Mock:

Với cách design như bài học, chúng ta cần viết test cho handler NoteCreate, chỗ này cần đến một khái niệm mock test

Bạn cần xài thư viện github.com/stretchr/testify/mock để tạo mock object cho việc xử lý db

Chúng ta sẽ cần implement lại NoteRepoImpl dưới dạng mock, chúng khá đơn giản như sau, nếu như bạn hiểu khái niệm.

Chú ý:

Chúng ta có 4 tình huống (test case) cần phải test.

  • title is required
  • title has min length = 6
  • title has max length = 100
  • title is valid
import (
	"../helper"
	"../model"
	"github.com/stretchr/testify/mock"
)

type NoteRepoImpl struct {
	mock.Mock
}

func (self *NoteRepoImpl) Create(note model.Note) (*model.Note, error) {
	args := self.Called(note)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	r := args.Get(0).(model.Note)
	return &r, args.Error(1)
}

// ... cac function khac nua

Sau khi có mock rồi, test hàm NoteCreate đơn giản sẽ được viết như sau:

Chú ý, bạn sẽ cần cả việc tìm hiểu thêm về việc giả lập gin.Context

Thêm nữa là, việc phải giả lập gin context cho thấy việc design phụ thuộc vô gin.Context cũng cũng là một việc làm cần phải suy nghĩ về việc liệu nó đã là tốt chưa. Đôi khi chúng ta phải trade off. Nhưng bạn có gom lại các dòng code về việc tạo gin.Context vào một function về testutil sẽ giúp cho code test sẽ trong sáng hơn về chỗ này.

func TestNoteCreateWithInValidValidator(t *testing.T) {
	gin.SetMode(gin.ReleaseMode)
	// 1. Gia lap context
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	data := `{"title":"todo"}`
	c.Request = httptest.NewRequest("POST", "/note", strings.NewReader(data))
	c.Request.Header.Set("Content-Type", "application/json")
	noteRepo := new(m.NoteRepoImpl)
	_, err := NoteCreate(c, noteRepo)
	if err == nil {
		t.Error("Error should not be nil because this will not valid min rule validator")
	}
}

func TestNoteCreateWithValidValidator(t *testing.T) {
	// 1. Gia lap context
	gin.SetMode(gin.ReleaseMode)
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	data := `{"title":"todo 123"}`
	c.Request = httptest.NewRequest("POST", "/note", strings.NewReader(data))
	c.Request.Header.Set("Content-Type", "application/json")
	// 2. Logic nay boc lo code thiet ke khong dung
	// 2.1 Do phai dung json.Unmarshal de thanh gia tri truyen vao ham On("Create")
	noteRepo := new(m.NoteRepoImpl)
	note := model.Note{}
	json.Unmarshal([]byte(data), &note)
	actual := model.Note{
		Title: "todo 123",
		Model: gorm.Model{
			ID:        123,
			CreatedAt: time.Now(),
			UpdatedAt: time.Now(),
		},
	}
	noteRepo.On("Create", note).Return(actual, nil)
	expected, err := NoteCreate(c, noteRepo)
	if err != nil {
		t.Error("Error should be nil")
	}
	if expected.ID != 123 {
		t.Error("note.ID should be 123")
	}
}

Gin Validation:

Ví dụ: title không được empty, min length là: 6, max length là 100

type Note struct {
	gorm.Model
	Title     string `binding:"required,min=6,max=100"`
	Completed bool
}

Gin sử dụng https://github.com/go-playground/validator v8, bạn có tham khảo các syntax khác ở đây.

Câu hỏi khó hơn để suy nghĩ là: làm thế nào để custom error message để thân thiện hơn hoặc là việt hoá

JWT

  • Tham khảo trong source code về các route như: /signin, /login để biết thêm chi tiết.
  • Chú ý tới đây các hàm thuộc về note cần có token gởi lên mơi được xử lý

Ví dụ:

  • Signin
curl --request POST \
  --url http://localhost:8081/signin \
  --header 'content-type: application/json' \
  --cookie Token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODU5MjgxNDUsImp0aSI6IjQiLCJpc3MiOiJOb3JkaWNDb2RlciJ9.v7nELcSyRcpiKtQqxdxS506T8eKZCcwfZglY7VBGUqo \
  --data '{
	"username": "tpphu4",
	"password": "12345678",
	"email": "tpphu4@gmail.com",
	"Fullname": "Phu Dep Trai"
}'
  • Login
curl --request POST \
  --url http://localhost:8081/login \
  --header 'content-type: application/json' \
  --cookie Token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODU5MjgxNDUsImp0aSI6IjQiLCJpc3MiOiJOb3JkaWNDb2RlciJ9.v7nELcSyRcpiKtQqxdxS506T8eKZCcwfZglY7VBGUqo \
  --data '{
	"login": "tpphu4",
	"password": "12345678"
}'
  • Create Note:
curl --request POST \
  --url http://localhost:8081/note \
  --header 'authentication: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODU5MjgxNDUsImp0aSI6IjQiLCJpc3MiOiJOb3JkaWNDb2RlciJ9.v7nELcSyRcpiKtQqxdxS506T8eKZCcwfZglY7VBGUqo' \
  --header 'content-type: application/json' \
  --cookie Token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODU5MjgxNDUsImp0aSI6IjQiLCJpc3MiOiJOb3JkaWNDb2RlciJ9.v7nELcSyRcpiKtQqxdxS506T8eKZCcwfZglY7VBGUqo \
  --data '{
	"title": "The func keyword signifies.",
	"completed": false
}'

Nginx Load Balancer

  • Thay cai host docker (chi danh cho Mac) host.docker.internal thanh local IP, vi du: 192.168.1.xxx
  • Cau hinh cai nay api.dev.local tro den 127.0.0.1
upstream api {
  server host.docker.internal:8081 weight=2;
  server host.docker.internal:8082 weight=1;
}

server {
  listen 80;
  listen [::]:80;
  server_name api.dev.local;
  location / {
      proxy_set_header   X-Forwarded-For $remote_addr;
      proxy_set_header   X-Scheme $scheme;
      proxy_set_header   Host $http_host;
      proxy_pass http://api;
  }
}

curl -X POST -H 'Content-Type: application/json' --data '{"Title": "Todo1", "Completed": true}' http://localhost:8088/note | jq .