├── .gitattributes ├── db ├── migration │ ├── 000001_init_schema.down.sql │ ├── 000002_add_users.down.sql │ ├── 000002_add_users.up.sql │ └── 000001_init_schema.up.sql ├── query │ ├── user.sql │ ├── entry.sql │ ├── transfer.sql │ └── account.sql ├── sqlc │ ├── main_test.go │ ├── models.go │ ├── querier.go │ ├── user.sql.go │ ├── user_test.go │ ├── entry_test.go │ ├── entry.sql.go │ ├── transfer_test.go │ ├── transfer.sql.go │ ├── account_test.go │ ├── store.go │ ├── account.sql.go │ ├── store_test.go │ └── db.go └── mock │ └── store.go ├── start.sh ├── app.env ├── eks ├── service.yaml ├── aws-auth.yaml ├── issuer.yaml ├── deployment.yaml └── ingress.yaml ├── util ├── currency.go ├── password.go ├── password_test.go ├── config.go └── random.go ├── api ├── validator.go ├── main_test.go ├── middleware.go ├── server.go ├── transfer.go ├── middleware_test.go ├── user.go ├── account_test.go ├── account.go ├── user_test.go └── transfer_test.go ├── token ├── maker.go ├── payload.go ├── paseto_maker_test.go ├── paseto_maker.go ├── jwt_maker.go └── jwt_maker_test.go ├── sqlc.yaml ├── Dockerfile ├── docker-compose.yaml ├── main.go ├── Makefile ├── LICENSE ├── .github └── workflows │ ├── test.yml │ └── deploy.yml ├── go.mod ├── wait-for.sh ├── README.md └── go.sum /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /db/migration/000001_init_schema.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS entries; 2 | DROP TABLE IF EXISTS transfers; 3 | DROP TABLE IF EXISTS accounts; -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "run db migration" 6 | source /app/app.env 7 | /app/migrate -path /app/migration -database "$DB_SOURCE" -verbose up 8 | 9 | echo "start the app" 10 | exec "$@" -------------------------------------------------------------------------------- /app.env: -------------------------------------------------------------------------------- 1 | DB_DRIVER=postgres 2 | DB_SOURCE=postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable 3 | SERVER_ADDRESS=0.0.0.0:8080 4 | TOKEN_SYMMETRIC_KEY=12345678901234567890123456789012 5 | ACCESS_TOKEN_DUATION=15m 6 | -------------------------------------------------------------------------------- /db/migration/000002_add_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE IF EXISTS "accounts" DROP CONSTRAINT IF EXISTS "owner_currency_key"; 2 | ALTER TABLE IF EXISTS "accounts" DROP CONSTRAINT IF EXISTS "accounts_owner_fkey"; 3 | DROP TABLE IF EXISTS "users"; 4 | -------------------------------------------------------------------------------- /eks/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: simple-bank-api-service 5 | spec: 6 | selector: 7 | app: simple-bank-api 8 | ports: 9 | - protocol: TCP 10 | port: 80 11 | targetPort: 8080 12 | type: ClusterIP -------------------------------------------------------------------------------- /db/query/user.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateUser :one 2 | INSERT INTO users ( 3 | username, 4 | hashed_password, 5 | full_name, 6 | email 7 | ) VALUES ( 8 | $1, $2, $3, $4 9 | ) RETURNING *; 10 | 11 | -- name: GetUser :one 12 | SELECT * FROM users 13 | WHERE username = $1 LIMIT 1; -------------------------------------------------------------------------------- /eks/aws-auth.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: aws-auth 5 | namespace: kube-system 6 | data: 7 | mapUsers: | 8 | - userarn: arn:aws:iam::963935081862:user/github-ci 9 | username: github-ci 10 | groups: 11 | - system:masters -------------------------------------------------------------------------------- /db/query/entry.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateEntry :one 2 | INSERT INTO entries ( 3 | account_id, 4 | amount 5 | ) VALUES ( 6 | $1, $2 7 | ) RETURNING *; 8 | 9 | -- name: GetEntry :one 10 | SELECT * FROM entries 11 | WHERE id = $1 LIMIT 1; 12 | 13 | -- name: ListEntries :many 14 | SELECT * FROM entries 15 | WHERE account_id = $1 16 | ORDER BY id 17 | LIMIT $2 18 | OFFSET $3; -------------------------------------------------------------------------------- /util/currency.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // Constants for all supported currencies 4 | const ( 5 | USD = "USD" 6 | EUR = "EUR" 7 | CAD = "CAD" 8 | ) 9 | 10 | // IsSupportedCurrency returns true if the currency is supported 11 | func IsSupportedCurrency(currency string) bool { 12 | switch currency { 13 | case USD, EUR, CAD: 14 | return true 15 | } 16 | return false 17 | } 18 | -------------------------------------------------------------------------------- /api/validator.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/CeciliaChen/simplebank/util" 5 | "github.com/go-playground/validator/v10" 6 | ) 7 | 8 | var validCurrency validator.Func = func(fieldLevel validator.FieldLevel) bool { 9 | if currency, ok := fieldLevel.Field().Interface().(string); ok { 10 | return util.IsSupportedCurrency(currency) 11 | } 12 | return false 13 | } 14 | -------------------------------------------------------------------------------- /token/maker.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import "time" 4 | 5 | // Maker is an interface for managing tokens 6 | type Maker interface { 7 | // CreateToken create a new tiken for a specific username and duration 8 | CreateToken(username string, duration time.Duration) (string, error) 9 | 10 | // VerifyToken checks if the token is valid or not 11 | VerifyToken(token string) (*Payload, error) 12 | } 13 | -------------------------------------------------------------------------------- /db/query/transfer.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateTransfer :one 2 | INSERT INTO transfers ( 3 | from_account_id, 4 | to_account_id, 5 | amount 6 | ) VALUES ( 7 | $1, $2, $3 8 | ) RETURNING *; 9 | 10 | -- name: GetTransfer :one 11 | SELECT * FROM transfers 12 | WHERE id = $1 LIMIT 1; 13 | 14 | -- name: ListTransfers :many 15 | SELECT * FROM transfers 16 | WHERE 17 | from_account_id = $1 OR 18 | to_account_id = $2 19 | ORDER BY id 20 | LIMIT $3 21 | OFFSET $4; -------------------------------------------------------------------------------- /eks/issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: ClusterIssuer 3 | metadata: 4 | name: letsencrypt 5 | spec: 6 | acme: 7 | email: chenguo0313@gmail.com 8 | server: https://acme-v02.api.letsencrypt.org/directory 9 | privateKeySecretRef: 10 | # Secret resource that will be used to store the account's private key. 11 | name: letsencrypt-account-private-key 12 | # Add a single challenge solver, HTTP01 using nginx 13 | solvers: 14 | - http01: 15 | ingress: 16 | ingressClassName: nginx -------------------------------------------------------------------------------- /sqlc.yaml: -------------------------------------------------------------------------------- 1 | # version: "2" 2 | # project: 3 | # id: "" 4 | # cloud: 5 | # organization: "" 6 | # project: "" 7 | # hostname: "" 8 | # sql: [] 9 | # overrides: 10 | # go: null 11 | # plugins: [] 12 | # rules: [] 13 | 14 | version: "1" 15 | packages: 16 | - name: "db" 17 | path: "./db/sqlc" 18 | queries: "./db/query/" 19 | schema: "./db/migration/" 20 | engine: "postgresql" 21 | emit_json_tags: true 22 | emit_prepared_queries: true 23 | emit_interface: true 24 | emit_exact_table_names: false 25 | emit_empty_slices: true -------------------------------------------------------------------------------- /eks/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: simple-bank-api-deployment 5 | labels: 6 | app: simple-bank-api 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: simple-bank-api 12 | template: 13 | metadata: 14 | labels: 15 | app: simple-bank-api 16 | spec: 17 | containers: 18 | - name: simple-bank-api 19 | image: 963935081862.dkr.ecr.us-east-1.amazonaws.com/simplebank:latest 20 | imagePullPolicy: Always 21 | ports: 22 | - containerPort: 8080 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.21-alpine3.19 AS builder 3 | WORKDIR /app 4 | COPY . . 5 | RUN go build -o main main.go 6 | RUN apk add curl 7 | RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz 8 | 9 | # Run stage 10 | FROM alpine:3.19 11 | WORKDIR /app 12 | COPY --from=builder /app/main . 13 | COPY --from=builder /app/migrate ./migrate 14 | COPY app.env . 15 | COPY start.sh . 16 | COPY wait-for.sh . 17 | COPY db/migration ./migration 18 | 19 | EXPOSE 8080 20 | CMD [ "/app/main" ] 21 | ENTRYPOINT [ "/app/start.sh" ] -------------------------------------------------------------------------------- /db/migration/000002_add_users.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "users" ( 2 | "username" varchar PRIMARY KEY, 3 | "hashed_password" varchar NOT NULL, 4 | "full_name" varchar NOT NULL, 5 | "email" varchar UNIQUE NOT NULL, 6 | "password_changed_at" timestamptz NOT NULL DEFAULT '0001-01-01 00:00:00Z', 7 | "created_at" timestamptz NOT NULL DEFAULT 'now()' 8 | ); 9 | 10 | ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username"); 11 | 12 | -- CREATE UNIQUE INDEX ON "accounts" ("owner", "currency"); 13 | ALTER TABLE "accounts" ADD CONSTRAINT "owner_currency_key" UNIQUE ("owner", "currency"); -------------------------------------------------------------------------------- /db/sqlc/main_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/CeciliaChen/simplebank/util" 10 | _ "github.com/lib/pq" 11 | ) 12 | 13 | var testQueries *Queries 14 | var testDB *sql.DB 15 | 16 | func TestMain(m *testing.M) { 17 | var err error 18 | config, err := util.LoadConfig("../..") //go to parent folder 19 | if err != nil { 20 | log.Fatal("cannot load config:", err) 21 | } 22 | 23 | testDB, err = sql.Open(config.DBDriver, config.DBSource) 24 | if err != nil { 25 | log.Fatal("cannot connect to db:", err) 26 | } 27 | 28 | testQueries = New(testDB) 29 | 30 | os.Exit(m.Run()) 31 | } 32 | -------------------------------------------------------------------------------- /api/main_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | db "github.com/CeciliaChen/simplebank/db/sqlc" 9 | "github.com/CeciliaChen/simplebank/util" 10 | "github.com/gin-gonic/gin" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func newTestServer(t *testing.T, store db.Store) *Server { 15 | config := util.Config{ 16 | TokenSymmetricKey: util.RandomString(32), 17 | AccessTokenDuration: time.Minute, 18 | } 19 | server, err := NewServer(config, store) 20 | require.NoError(t, err) 21 | 22 | return server 23 | } 24 | 25 | func TestMain(m *testing.M) { 26 | gin.SetMode(gin.TestMode) 27 | os.Exit(m.Run()) 28 | } 29 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | postgres: 4 | image: postgres:12-alpine 5 | environment: 6 | - POSTGRES_USER=root 7 | - POSTGRES_PASSWORD=secret 8 | - POSTGRES_DB=simple_bank 9 | api: 10 | build: 11 | context: . 12 | dockerfile: Dockerfile 13 | ports: 14 | - "8080:8080" 15 | environment: 16 | - DB_SOURCE=postgresql://root:secret@postgres:5432/simple_bank?sslmode=disable 17 | depends_on: 18 | - postgres 19 | entrypoint: 20 | [ 21 | "/app/wait-for.sh", 22 | "postgres:5432", 23 | "--", 24 | "/app/start.sh" 25 | ] 26 | command: [ "/app/main" ] 27 | -------------------------------------------------------------------------------- /util/password.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/crypto/bcrypt" 7 | ) 8 | 9 | // HashPassword returns the bcrypt hash of the password 10 | func HashPassword(password string) (string, error) { 11 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 12 | if err != nil { 13 | return "", fmt.Errorf("failed to hash password: %w", err) 14 | } 15 | return string(hashedPassword), nil 16 | } 17 | 18 | // Checkpassword checks if the provided password is correct or not 19 | func CheckPassword(password string, hashedPassword string) error { 20 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 21 | } 22 | -------------------------------------------------------------------------------- /eks/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: IngressClass 3 | metadata: 4 | name: nginx 5 | spec: 6 | controller: k8s.io/ingress-nginx 7 | --- 8 | apiVersion: networking.k8s.io/v1 9 | kind: Ingress 10 | metadata: 11 | name: simple-bank-ingress 12 | annotations: 13 | cert-manager.io/cluster-issuer: letsencrypt 14 | spec: 15 | ingressClassName: nginx 16 | rules: 17 | - host: "api.ceci-simple-bank.com" 18 | http: 19 | paths: 20 | - pathType: Prefix 21 | path: "/" 22 | backend: 23 | service: 24 | name: simple-bank-api-service 25 | port: 26 | number: 80 27 | tls: 28 | - hosts: 29 | - api.ceci-simple-bank.com 30 | secretName: simple-bank-api-cert -------------------------------------------------------------------------------- /util/password_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "golang.org/x/crypto/bcrypt" 8 | ) 9 | 10 | func TestPassword(t *testing.T) { 11 | password := RandomString(6) 12 | 13 | hashedPassword1, err := HashPassword(password) 14 | require.NoError(t, err) 15 | require.NotEmpty(t, hashedPassword1) 16 | 17 | err = CheckPassword(password, hashedPassword1) 18 | require.NoError(t, err) 19 | 20 | wrongPassword := RandomString(6) 21 | err = CheckPassword(wrongPassword, hashedPassword1) 22 | require.EqualError(t, err, bcrypt.ErrMismatchedHashAndPassword.Error()) 23 | 24 | hashedPassword2, err := HashPassword(password) 25 | require.NoError(t, err) 26 | require.NotEmpty(t, hashedPassword2) 27 | require.NotEqual(t, hashedPassword1, hashedPassword2) 28 | } 29 | -------------------------------------------------------------------------------- /db/query/account.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateAccount :one 2 | INSERT INTO accounts ( 3 | owner, 4 | balance, 5 | currency 6 | ) VALUES ( 7 | $1, $2, $3 8 | ) RETURNING *; 9 | 10 | -- name: GetAccount :one 11 | SELECT * FROM accounts 12 | WHERE id = $1 LIMIT 1; 13 | 14 | -- name: GetAccountForUpdate :one 15 | SELECT * FROM accounts 16 | WHERE id = $1 LIMIT 1 17 | FOR NO KEY UPDATE; 18 | 19 | -- name: ListAccounts :many 20 | SELECT * FROM accounts 21 | WHERE owner = $1 22 | ORDER BY id 23 | LIMIT $2 24 | OFFSET $3; 25 | 26 | -- name: UpdateAccount :one 27 | UPDATE accounts 28 | SET balance = $2 29 | WHERE id = $1 30 | RETURNING *; 31 | 32 | -- name: AddAccountBalance :one 33 | UPDATE accounts 34 | SET balance = balance + sqlc.arg(amount) 35 | WHERE id = sqlc.arg(id) 36 | RETURNING *; 37 | 38 | -- name: DeleteAccount :exec 39 | DELETE FROM accounts 40 | WHERE id = $1; -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/CeciliaChen/simplebank/api" // Replace with the actual import path for api 8 | db "github.com/CeciliaChen/simplebank/db/sqlc" 9 | "github.com/CeciliaChen/simplebank/util" 10 | _ "github.com/lib/pq" // This is the driver for PostgreSQL 11 | ) 12 | 13 | func main() { 14 | config, err := util.LoadConfig(".") 15 | if err != nil { 16 | log.Fatal("cannot load config:", err) 17 | } 18 | 19 | conn, err := sql.Open(config.DBDriver, config.DBSource) 20 | if err != nil { 21 | log.Fatal("cannot connect to db:", err) 22 | } 23 | 24 | store := db.NewStore(conn) 25 | server, err := api.NewServer(config, store) 26 | if err != nil { 27 | log.Fatal("cannot create server:", err) 28 | } 29 | 30 | err = server.Start(config.ServerAddress) 31 | if err != nil { 32 | log.Fatal("cannot start server:", err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /util/config.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // Config stores all configuration of the application 10 | // The values are read by viper from a config file or environment variables. 11 | type Config struct { 12 | DBDriver string `mapstructure:"DB_DRIVER"` 13 | DBSource string `mapstructure:"DB_SOURCE"` 14 | ServerAddress string `mapstructure:"SERVER_ADDRESS"` 15 | TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"` 16 | AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DUATION"` 17 | } 18 | 19 | // LoadConfig reads configuration from file or environment variables 20 | func LoadConfig(path string) (config Config, err error) { 21 | viper.AddConfigPath(path) 22 | viper.SetConfigName("app") 23 | viper.SetConfigType("env") 24 | 25 | viper.AutomaticEnv() 26 | 27 | err = viper.ReadInConfig() 28 | if err != nil { 29 | return 30 | } 31 | 32 | err = viper.Unmarshal(&config) 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /token/payload.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // Different types of errors returned by the VerifyToken function 11 | var ( 12 | ErrInvalidToken = errors.New("token is invalid") 13 | ErrExpiredToken = errors.New("token has expired") 14 | ) 15 | 16 | // Payload contains the payload data of the token 17 | type Payload struct { 18 | ID uuid.UUID `json:"id"` 19 | Username string `json:"username"` 20 | IssuedAt time.Time `json:"issued_at"` 21 | ExpiredAt time.Time `json:"expired_at"` 22 | } 23 | 24 | // NewPayload creates a new token payload with a specfici username and duration 25 | func NewPayload(username string, duration time.Duration) (*Payload, error) { 26 | tokenID, err := uuid.NewRandom() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | payload := &Payload{ 32 | ID: tokenID, 33 | Username: username, 34 | IssuedAt: time.Now(), 35 | ExpiredAt: time.Now().Add(duration), 36 | } 37 | return payload, nil 38 | } 39 | 40 | // Valid checks if the token payload is valid or not 41 | func (payload *Payload) Valid() error { 42 | if time.Now().After(payload.ExpiredAt) { 43 | return ErrExpiredToken 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | postgres: 2 | docker run --name postgres12 --network bank-network -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:12-alpine 3 | 4 | createdb: 5 | docker exec -it postgres12 createdb --username=root --owner=root simple_bank 6 | 7 | dropdb: 8 | docker exec -it postgres12 dropdb simple_bank 9 | 10 | migrateup: 11 | migrate -path db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose up 12 | 13 | migrateup1: 14 | migrate -path db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose up 1 15 | 16 | migratedown: 17 | migrate -path db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose down 18 | 19 | migratedown1: 20 | migrate -path db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose down 1 21 | 22 | sqlc: 23 | sqlc generate 24 | 25 | test: 26 | go test -v -cover ./... 27 | 28 | server: 29 | go run main.go 30 | 31 | mock: 32 | mockgen -package mockdb -destination db/mock/store.go github.com/CeciliaChen/simplebank/db/sqlc Store 33 | 34 | .PHONY: postgres createdb dropdb migrateup migratedown migrateup1 migratedown1 sqlc test server mock 35 | -------------------------------------------------------------------------------- /db/sqlc/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.22.0 4 | 5 | package db 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | type Account struct { 12 | ID int64 `json:"id"` 13 | Owner string `json:"owner"` 14 | Balance int64 `json:"balance"` 15 | Currency string `json:"currency"` 16 | CreatedAt time.Time `json:"created_at"` 17 | } 18 | 19 | type Entry struct { 20 | ID int64 `json:"id"` 21 | AccountID int64 `json:"account_id"` 22 | // can be nagative or positive 23 | Amount int64 `json:"amount"` 24 | CreatedAt time.Time `json:"created_at"` 25 | } 26 | 27 | type Transfer struct { 28 | ID int64 `json:"id"` 29 | FromAccountID int64 `json:"from_account_id"` 30 | ToAccountID int64 `json:"to_account_id"` 31 | // must be positive 32 | Amount int64 `json:"amount"` 33 | CreatedAt time.Time `json:"created_at"` 34 | } 35 | 36 | type User struct { 37 | Username string `json:"username"` 38 | HashedPassword string `json:"hashed_password"` 39 | FullName string `json:"full_name"` 40 | Email string `json:"email"` 41 | PasswordChangedAt time.Time `json:"password_changed_at"` 42 | CreatedAt time.Time `json:"created_at"` 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Original Work 4 | Copyright (c) 2016 Matthias Kadenbach 5 | https://github.com/mattes/migrate 6 | 7 | Modified Work 8 | Copyright (c) 2018 Dale Hui 9 | https://github.com/golang-migrate/migrate 10 | 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in 20 | all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | THE SOFTWARE. 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | # Label used to access the service container 16 | postgres: 17 | image: postgres:12 18 | env: 19 | POSTGRES_USER: root 20 | POSTGRES_DB: simple_bank 21 | POSTGRES_PASSWORD: secret 22 | options: >- 23 | --health-cmd pg_isready 24 | --health-interval 10s 25 | --health-timeout 5s 26 | ports: 27 | - 5432:5432 28 | 29 | steps: 30 | - name: Set up Go 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: '1.21' 34 | id: go 35 | 36 | - name: Check out code into the Go module directory 37 | uses: actions/checkout@v4 38 | 39 | - name: Install golang-migrate 40 | run: | 41 | curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz 42 | sudo mv migrate /usr/bin/ 43 | which migrate 44 | 45 | - name: Run migrations 46 | run: make migrateup 47 | 48 | - name: Test 49 | run: make test 50 | -------------------------------------------------------------------------------- /util/random.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func init() { 11 | rand.Seed(time.Now().UnixNano()) 12 | } 13 | 14 | // RandomInt generates a random integer between min and max 15 | func RandomInt(min, max int64) int64 { 16 | return min + rand.Int63n(max-min+1) 17 | } 18 | 19 | const alphabet = "abcdefghijkmlnopqrstuvwxyz" 20 | 21 | // RandomInt generates a random integer between min and max 22 | func RandomString(n int) string { 23 | var sb strings.Builder 24 | k := len(alphabet) 25 | 26 | for i := 0; i < n; i++ { 27 | c := alphabet[rand.Intn(k)] 28 | sb.WriteByte(c) 29 | } 30 | 31 | return sb.String() 32 | } 33 | 34 | // RandomOwner generates a random owner name in alphabet 35 | func RandomOwner() string { 36 | return RandomString(6) 37 | } 38 | 39 | // RandomMoney generates a random money number from 0 to 1000 40 | func RandomMoney() int64 { 41 | return RandomInt(0, 1000) 42 | } 43 | 44 | // RandomCurrency generates a random currencies between 3 currencies 45 | func RandomCurrency() string { 46 | currencies := []string{EUR, USD, CAD} 47 | n := len(currencies) 48 | return currencies[rand.Intn(n)] 49 | } 50 | 51 | // RandomEmail generates a random email 52 | func RandomEmail() string { 53 | return fmt.Sprintf("%s@email.com", RandomString(6)) 54 | } 55 | -------------------------------------------------------------------------------- /db/sqlc/querier.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.22.0 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | ) 10 | 11 | type Querier interface { 12 | AddAccountBalance(ctx context.Context, arg AddAccountBalanceParams) (Account, error) 13 | CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) 14 | CreateEntry(ctx context.Context, arg CreateEntryParams) (Entry, error) 15 | CreateTransfer(ctx context.Context, arg CreateTransferParams) (Transfer, error) 16 | CreateUser(ctx context.Context, arg CreateUserParams) (User, error) 17 | DeleteAccount(ctx context.Context, id int64) error 18 | GetAccount(ctx context.Context, id int64) (Account, error) 19 | GetAccountForUpdate(ctx context.Context, id int64) (Account, error) 20 | GetEntry(ctx context.Context, id int64) (Entry, error) 21 | GetTransfer(ctx context.Context, id int64) (Transfer, error) 22 | GetUser(ctx context.Context, username string) (User, error) 23 | ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) 24 | ListEntries(ctx context.Context, arg ListEntriesParams) ([]Entry, error) 25 | ListTransfers(ctx context.Context, arg ListTransfersParams) ([]Transfer, error) 26 | UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) 27 | } 28 | 29 | var _ Querier = (*Queries)(nil) 30 | -------------------------------------------------------------------------------- /token/paseto_maker_test.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/CeciliaChen/simplebank/util" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestPasetoMaker(t *testing.T) { 12 | maker, err := NewPasetoMaker(util.RandomString(32)) 13 | require.NoError(t, err) 14 | 15 | username := util.RandomOwner() 16 | duration := time.Minute 17 | 18 | issuedAt := time.Now() 19 | expiredAt := issuedAt.Add(duration) 20 | 21 | token, err := maker.CreateToken(username, duration) 22 | require.NoError(t, err) 23 | require.NotEmpty(t, token) 24 | 25 | payload, err := maker.VerifyToken(token) 26 | require.NoError(t, err) 27 | require.NotEmpty(t, payload) 28 | 29 | require.NotZero(t, payload.ID) 30 | require.Equal(t, username, payload.Username) 31 | require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) 32 | require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) 33 | 34 | } 35 | 36 | func TestExpiredPasetoToken(t *testing.T) { 37 | maker, err := NewPasetoMaker(util.RandomString(32)) 38 | require.NoError(t, err) 39 | 40 | token, err := maker.CreateToken(util.RandomOwner(), -time.Minute) 41 | require.NoError(t, err) 42 | require.NotEmpty(t, token) 43 | 44 | payload, err := maker.VerifyToken(token) 45 | require.Error(t, err) 46 | require.EqualError(t, err, ErrExpiredToken.Error()) 47 | require.Nil(t, payload) 48 | } 49 | -------------------------------------------------------------------------------- /db/migration/000001_init_schema.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "accounts" ( 2 | "id" bigserial PRIMARY KEY, 3 | "owner" varchar NOT NULL, 4 | "balance" bigint NOT NULL, 5 | "currency" varchar NOT NULL, 6 | "created_at" timestamptz NOT NULL DEFAULT 'now()' 7 | ); 8 | 9 | CREATE TABLE "entries" ( 10 | "id" bigserial PRIMARY KEY, 11 | "account_id" bigint NOT NULL, 12 | "amount" bigint NOT NULL, 13 | "created_at" timestamptz NOT NULL DEFAULT 'now()' 14 | ); 15 | 16 | CREATE TABLE "transfers" ( 17 | "id" bigserial PRIMARY KEY, 18 | "from_account_id" bigint NOT NULL, 19 | "to_account_id" bigint NOT NULL, 20 | "amount" bigint NOT NULL, 21 | "created_at" timestamptz NOT NULL DEFAULT 'now()' 22 | ); 23 | 24 | CREATE INDEX ON "accounts" ("owner"); 25 | 26 | CREATE INDEX ON "entries" ("account_id"); 27 | 28 | CREATE INDEX ON "transfers" ("from_account_id"); 29 | 30 | CREATE INDEX ON "transfers" ("to_account_id"); 31 | 32 | CREATE INDEX ON "transfers" ("from_account_id", "to_account_id"); 33 | 34 | COMMENT ON COLUMN "entries"."amount" IS 'can be nagative or positive'; 35 | 36 | COMMENT ON COLUMN "transfers"."amount" IS 'must be positive'; 37 | 38 | ALTER TABLE "entries" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id"); 39 | 40 | ALTER TABLE "transfers" ADD FOREIGN KEY ("from_account_id") REFERENCES "accounts" ("id"); 41 | 42 | ALTER TABLE "transfers" ADD FOREIGN KEY ("to_account_id") REFERENCES "accounts" ("id"); 43 | -------------------------------------------------------------------------------- /token/paseto_maker.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/aead/chacha20poly1305" 8 | "github.com/o1egl/paseto" 9 | ) 10 | 11 | // PasetoMaker is a PASETO token maker 12 | type PasetoMaker struct { 13 | paseto *paseto.V2 14 | symmetricKey []byte 15 | } 16 | 17 | // NewPasetoMaker creates a new PasetoMaker 18 | func NewPasetoMaker(symmetricKey string) (Maker, error) { 19 | if len(symmetricKey) != chacha20poly1305.KeySize { 20 | return nil, fmt.Errorf("invalid key size: must be exactly %d characters", chacha20poly1305.KeySize) 21 | } 22 | 23 | maker := &PasetoMaker{ 24 | paseto: paseto.NewV2(), 25 | symmetricKey: []byte(symmetricKey), 26 | } 27 | return maker, nil 28 | } 29 | 30 | // CreateToken create a new tiken for a specific username and duration 31 | func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, error) { 32 | payload, err := NewPayload(username, duration) 33 | if err != nil { 34 | return "", nil 35 | } 36 | return maker.paseto.Encrypt(maker.symmetricKey, payload, nil) 37 | } 38 | 39 | // VerifyToken checks if the token is valid or not 40 | func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) { 41 | payload := &Payload{} 42 | 43 | err := maker.paseto.Decrypt(token, maker.symmetricKey, payload, nil) 44 | if err != nil { 45 | return nil, ErrInvalidToken 46 | } 47 | 48 | err = payload.Valid() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return payload, nil 54 | } 55 | -------------------------------------------------------------------------------- /api/middleware.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/CeciliaChen/simplebank/token" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | const ( 15 | authorizationHeaderKey = "authorization" 16 | authorizationTypeBearer = "bearer" 17 | authorizationPayloadKey = "authorization_payload" 18 | ) 19 | 20 | func authMiddleware(tokenMaker token.Maker) gin.HandlerFunc { 21 | return func(ctx *gin.Context) { 22 | authorizationHeader := ctx.GetHeader(authorizationHeaderKey) 23 | if len(authorizationHeader) == 0 { 24 | err := errors.New("authorization header is not provided") 25 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err)) 26 | return 27 | } 28 | fields := strings.Fields(authorizationHeader) 29 | 30 | if len(fields) < 2 { 31 | err := errors.New("invalid authorization header format") 32 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err)) 33 | return 34 | } 35 | 36 | authorizationType := strings.ToLower(fields[0]) 37 | if authorizationType != authorizationTypeBearer { 38 | err := fmt.Errorf("unsupported authorization type %s", authorizationType) 39 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err)) 40 | return 41 | } 42 | 43 | accessToken := fields[1] 44 | payload, err := tokenMaker.VerifyToken(accessToken) 45 | if err != nil { 46 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err)) 47 | return 48 | } 49 | 50 | ctx.Set(authorizationPayloadKey, payload) 51 | ctx.Next() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /db/sqlc/user.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.22.0 4 | // source: user.sql 5 | 6 | package db 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const createUser = `-- name: CreateUser :one 13 | INSERT INTO users ( 14 | username, 15 | hashed_password, 16 | full_name, 17 | email 18 | ) VALUES ( 19 | $1, $2, $3, $4 20 | ) RETURNING username, hashed_password, full_name, email, password_changed_at, created_at 21 | ` 22 | 23 | type CreateUserParams struct { 24 | Username string `json:"username"` 25 | HashedPassword string `json:"hashed_password"` 26 | FullName string `json:"full_name"` 27 | Email string `json:"email"` 28 | } 29 | 30 | func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { 31 | row := q.queryRow(ctx, q.createUserStmt, createUser, 32 | arg.Username, 33 | arg.HashedPassword, 34 | arg.FullName, 35 | arg.Email, 36 | ) 37 | var i User 38 | err := row.Scan( 39 | &i.Username, 40 | &i.HashedPassword, 41 | &i.FullName, 42 | &i.Email, 43 | &i.PasswordChangedAt, 44 | &i.CreatedAt, 45 | ) 46 | return i, err 47 | } 48 | 49 | const getUser = `-- name: GetUser :one 50 | SELECT username, hashed_password, full_name, email, password_changed_at, created_at FROM users 51 | WHERE username = $1 LIMIT 1 52 | ` 53 | 54 | func (q *Queries) GetUser(ctx context.Context, username string) (User, error) { 55 | row := q.queryRow(ctx, q.getUserStmt, getUser, username) 56 | var i User 57 | err := row.Scan( 58 | &i.Username, 59 | &i.HashedPassword, 60 | &i.FullName, 61 | &i.Email, 62 | &i.PasswordChangedAt, 63 | &i.CreatedAt, 64 | ) 65 | return i, err 66 | } 67 | -------------------------------------------------------------------------------- /db/sqlc/user_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/CeciliaChen/simplebank/util" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func createRandomUser(t *testing.T) User { 13 | hashedPassword, err := util.HashPassword(util.RandomString(6)) 14 | require.NoError(t, err) 15 | 16 | arg := CreateUserParams{ 17 | Username: util.RandomOwner(), 18 | HashedPassword: hashedPassword, 19 | FullName: util.RandomOwner(), 20 | Email: util.RandomEmail(), 21 | } 22 | 23 | user, err := testQueries.CreateUser(context.Background(), arg) 24 | require.NoError(t, err) 25 | require.NotEmpty(t, user) 26 | 27 | require.Equal(t, arg.Username, user.Username) 28 | require.Equal(t, arg.HashedPassword, user.HashedPassword) 29 | require.Equal(t, arg.FullName, user.FullName) 30 | require.Equal(t, arg.Email, user.Email) 31 | 32 | require.True(t, user.PasswordChangedAt.IsZero()) 33 | require.NotZero(t, user.CreatedAt) 34 | 35 | return user 36 | } 37 | 38 | func TestCreateUser(t *testing.T) { 39 | createRandomUser(t) 40 | } 41 | 42 | func TestGetUser(t *testing.T) { 43 | user1 := createRandomUser(t) 44 | user2, err := testQueries.GetUser(context.Background(), user1.Username) 45 | require.NoError(t, err) 46 | require.NotEmpty(t, user2) 47 | 48 | require.Equal(t, user1.Username, user2.Username) 49 | require.Equal(t, user1.HashedPassword, user2.HashedPassword) 50 | require.Equal(t, user1.FullName, user2.FullName) 51 | require.Equal(t, user1.Email, user2.Email) 52 | require.WithinDuration(t, user1.CreatedAt, user2.CreatedAt, time.Second) 53 | require.WithinDuration(t, user1.PasswordChangedAt, user2.PasswordChangedAt, time.Second) 54 | } 55 | -------------------------------------------------------------------------------- /token/jwt_maker.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/dgrijalva/jwt-go" 9 | ) 10 | 11 | const minSecretKeySize = 32 12 | 13 | // JWTMaker is a JSON Web Token maker 14 | type JWTMaker struct { 15 | secretKey string 16 | } 17 | 18 | // NewJWTMaker creates a new JWTMaker 19 | func NewJWTMaker(secretKey string) (Maker, error) { 20 | if len(secretKey) < minSecretKeySize { 21 | return nil, fmt.Errorf("invalid key size: must be at least %d characters", minSecretKeySize) 22 | } 23 | 24 | return &JWTMaker{secretKey}, nil 25 | } 26 | 27 | // CreateToken create a new tiken for a specific username and duration 28 | func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, error) { 29 | payload, err := NewPayload(username, duration) 30 | if err != nil { 31 | return "", err 32 | } 33 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) 34 | return jwtToken.SignedString([]byte(maker.secretKey)) 35 | } 36 | 37 | // VerifyToken checks if the token is valid or not 38 | func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) { 39 | keyFunc := func(token *jwt.Token) (interface{}, error) { 40 | _, ok := token.Method.(*jwt.SigningMethodHMAC) 41 | if !ok { 42 | return nil, jwt.ErrInvalidKey 43 | } 44 | return []byte(maker.secretKey), nil 45 | } 46 | 47 | jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc) 48 | if err != nil { 49 | verr, ok := err.(*jwt.ValidationError) 50 | if ok && errors.Is(verr.Inner, ErrExpiredToken) { 51 | return nil, ErrExpiredToken 52 | } 53 | return nil, ErrInvalidToken 54 | } 55 | 56 | payload, ok := jwtToken.Claims.(*Payload) 57 | if !ok { 58 | return nil, ErrInvalidToken 59 | } 60 | 61 | return payload, nil 62 | } 63 | -------------------------------------------------------------------------------- /db/sqlc/entry_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/CeciliaChen/simplebank/util" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func createRandomEntry(t *testing.T, account Account) Entry { 13 | arg := CreateEntryParams{ 14 | AccountID: account.ID, 15 | Amount: util.RandomMoney(), 16 | } 17 | 18 | entry, err := testQueries.CreateEntry(context.Background(), arg) 19 | require.NoError(t, err) 20 | require.NotEmpty(t, entry) 21 | 22 | require.Equal(t, arg.AccountID, entry.AccountID) 23 | require.Equal(t, arg.Amount, entry.Amount) 24 | 25 | require.NotZero(t, entry.ID) 26 | require.NotZero(t, entry.CreatedAt) 27 | 28 | return entry 29 | } 30 | 31 | func TestCreateEntry(t *testing.T) { 32 | account := createRandomAccount(t) 33 | createRandomEntry(t, account) 34 | } 35 | 36 | func TestGetEntry(t *testing.T) { 37 | account := createRandomAccount(t) 38 | entry1 := createRandomEntry(t, account) 39 | entry2, err := testQueries.GetEntry(context.Background(), entry1.ID) 40 | require.NoError(t, err) 41 | require.NotEmpty(t, entry2) 42 | 43 | require.Equal(t, entry1.ID, entry2.ID) 44 | require.Equal(t, entry1.AccountID, entry2.AccountID) 45 | require.Equal(t, entry1.Amount, entry2.Amount) 46 | require.WithinDuration(t, entry1.CreatedAt, entry2.CreatedAt, time.Second) 47 | } 48 | 49 | func TestListEntries(t *testing.T) { 50 | account := createRandomAccount(t) 51 | for i := 0; i < 10; i++ { 52 | createRandomEntry(t, account) 53 | } 54 | 55 | arg := ListEntriesParams{ 56 | AccountID: account.ID, 57 | Limit: 5, 58 | Offset: 5, 59 | } 60 | 61 | entries, err := testQueries.ListEntries(context.Background(), arg) 62 | require.NoError(t, err) 63 | require.Len(t, entries, 5) 64 | 65 | for _, entry := range entries { 66 | require.NotEmpty(t, entry) 67 | require.Equal(t, arg.AccountID, entry.AccountID) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to production 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | deploy: 9 | name: Build image 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v4 15 | 16 | - name: Install kubectl 17 | uses: azure/setup-kubectl@v3 18 | with: 19 | version: 'v1.29.0' 20 | id: install 21 | 22 | - name: Configure AWS credentials 23 | uses: aws-actions/configure-aws-credentials@v1 24 | with: 25 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 26 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 27 | aws-region: us-east-1 28 | 29 | - name: Login to Amazon ECR 30 | id: login-ecr 31 | uses: aws-actions/amazon-ecr-login@v1 32 | 33 | - name: Load secrets and save to app.env 34 | run: aws secretsmanager get-secret-value --secret-id simplebank --query SecretString --output text | jq -r 'to_entries|map("\(.key)=\(.value)")|.[]' > app.env 35 | 36 | - name: Build, tag, and push docker image to Amazon ECR 37 | env: 38 | REGISTRY: ${{ steps.login-ecr.outputs.registry }} 39 | REPOSITORY: simplebank 40 | IMAGE_TAG: ${{ github.sha }} 41 | run: | 42 | docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG -t $REGISTRY/$REPOSITORY:latest . 43 | docker push -a $REGISTRY/$REPOSITORY 44 | 45 | - name: Update kube config 46 | run: aws eks update-kubeconfig --name simplebank --region us-east-1 47 | 48 | - name: Deploy image to Amazon EKS 49 | run: | 50 | kubectl apply -f eks/aws-auth.yaml 51 | kubectl apply -f eks/deployment.yaml 52 | kubectl apply -f eks/service.yaml 53 | kubectl apply -f eks/issuer.yaml 54 | kubectl apply -f eks/ingress.yaml 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /token/jwt_maker_test.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/CeciliaChen/simplebank/util" 8 | "github.com/dgrijalva/jwt-go" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestJWTMaker(t *testing.T) { 13 | maker, err := NewJWTMaker(util.RandomString(32)) 14 | require.NoError(t, err) 15 | 16 | username := util.RandomOwner() 17 | duration := time.Minute 18 | 19 | issuedAt := time.Now() 20 | expiredAt := issuedAt.Add(duration) 21 | 22 | token, err := maker.CreateToken(username, duration) 23 | require.NoError(t, err) 24 | require.NotEmpty(t, token) 25 | 26 | payload, err := maker.VerifyToken(token) 27 | require.NoError(t, err) 28 | require.NotEmpty(t, payload) 29 | 30 | require.NotZero(t, payload.ID) 31 | require.Equal(t, username, payload.Username) 32 | require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) 33 | require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) 34 | 35 | } 36 | 37 | func TestExpiredJWTToken(t *testing.T) { 38 | maker, err := NewJWTMaker(util.RandomString(32)) 39 | require.NoError(t, err) 40 | 41 | token, err := maker.CreateToken(util.RandomOwner(), -time.Minute) 42 | require.NoError(t, err) 43 | require.NotEmpty(t, token) 44 | 45 | payload, err := maker.VerifyToken(token) 46 | require.Error(t, err) 47 | require.EqualError(t, err, ErrExpiredToken.Error()) 48 | require.Nil(t, payload) 49 | } 50 | 51 | func TestInvalidJWTTokenAlgNone(t *testing.T) { 52 | payload, err := NewPayload(util.RandomOwner(), time.Minute) 53 | require.NoError(t, err) 54 | 55 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, payload) 56 | token, err := jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType) 57 | require.NoError(t, err) 58 | 59 | maker, err := NewJWTMaker(util.RandomString(32)) 60 | require.NoError(t, err) 61 | 62 | payload, err = maker.VerifyToken(token) 63 | require.Error(t, err) 64 | require.EqualError(t, err, ErrInvalidToken.Error()) 65 | require.Nil(t, payload) 66 | } 67 | -------------------------------------------------------------------------------- /api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | 6 | db "github.com/CeciliaChen/simplebank/db/sqlc" 7 | "github.com/CeciliaChen/simplebank/token" 8 | "github.com/CeciliaChen/simplebank/util" 9 | "github.com/gin-gonic/gin" 10 | "github.com/gin-gonic/gin/binding" 11 | "github.com/go-playground/validator/v10" 12 | ) 13 | 14 | // Server serves HTTP requests for the banking service 15 | type Server struct { 16 | config util.Config 17 | store db.Store 18 | tokenMaker token.Maker 19 | router *gin.Engine 20 | } 21 | 22 | // NewServer creates a new HTTP server and setup routing. 23 | func NewServer(config util.Config, store db.Store) (*Server, error) { 24 | tokenMaker, err := token.NewPasetoMaker(config.TokenSymmetricKey) 25 | if err != nil { 26 | return nil, fmt.Errorf("cannot create token maker: %w", err) 27 | } 28 | 29 | server := &Server{ 30 | config: config, 31 | store: store, 32 | tokenMaker: tokenMaker, 33 | } 34 | 35 | if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 36 | v.RegisterValidation("currency", validCurrency) 37 | } 38 | 39 | server.setupRouter() 40 | return server, nil 41 | } 42 | 43 | func (server *Server) setupRouter() { 44 | router := gin.Default() 45 | 46 | router.POST("/users", server.createUser) 47 | router.POST("/users/login", server.loginUser) 48 | 49 | authRoutes := router.Group("/").Use(authMiddleware(server.tokenMaker)) 50 | 51 | authRoutes.POST("/accounts", server.createAccount) 52 | authRoutes.GET("/accounts/:id", server.getAccount) 53 | authRoutes.GET("/accounts", server.listAccount) 54 | authRoutes.DELETE("/accounts/:id", server.deleteAccount) 55 | authRoutes.PUT("/accounts/:id/balance", server.addAccountBalance) 56 | authRoutes.PUT("/accounts/:id", server.updateAccount) 57 | 58 | authRoutes.POST("/transfers", server.createTransfer) 59 | 60 | server.router = router 61 | } 62 | 63 | // Start runs the HTTP server on a specific address 64 | func (server *Server) Start(address string) error { 65 | return server.router.Run(address) 66 | } 67 | 68 | func errorResponse(err error) gin.H { 69 | return gin.H{"error": err.Error()} 70 | } 71 | -------------------------------------------------------------------------------- /db/sqlc/entry.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.22.0 4 | // source: entry.sql 5 | 6 | package db 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const createEntry = `-- name: CreateEntry :one 13 | INSERT INTO entries ( 14 | account_id, 15 | amount 16 | ) VALUES ( 17 | $1, $2 18 | ) RETURNING id, account_id, amount, created_at 19 | ` 20 | 21 | type CreateEntryParams struct { 22 | AccountID int64 `json:"account_id"` 23 | Amount int64 `json:"amount"` 24 | } 25 | 26 | func (q *Queries) CreateEntry(ctx context.Context, arg CreateEntryParams) (Entry, error) { 27 | row := q.queryRow(ctx, q.createEntryStmt, createEntry, arg.AccountID, arg.Amount) 28 | var i Entry 29 | err := row.Scan( 30 | &i.ID, 31 | &i.AccountID, 32 | &i.Amount, 33 | &i.CreatedAt, 34 | ) 35 | return i, err 36 | } 37 | 38 | const getEntry = `-- name: GetEntry :one 39 | SELECT id, account_id, amount, created_at FROM entries 40 | WHERE id = $1 LIMIT 1 41 | ` 42 | 43 | func (q *Queries) GetEntry(ctx context.Context, id int64) (Entry, error) { 44 | row := q.queryRow(ctx, q.getEntryStmt, getEntry, id) 45 | var i Entry 46 | err := row.Scan( 47 | &i.ID, 48 | &i.AccountID, 49 | &i.Amount, 50 | &i.CreatedAt, 51 | ) 52 | return i, err 53 | } 54 | 55 | const listEntries = `-- name: ListEntries :many 56 | SELECT id, account_id, amount, created_at FROM entries 57 | WHERE account_id = $1 58 | ORDER BY id 59 | LIMIT $2 60 | OFFSET $3 61 | ` 62 | 63 | type ListEntriesParams struct { 64 | AccountID int64 `json:"account_id"` 65 | Limit int32 `json:"limit"` 66 | Offset int32 `json:"offset"` 67 | } 68 | 69 | func (q *Queries) ListEntries(ctx context.Context, arg ListEntriesParams) ([]Entry, error) { 70 | rows, err := q.query(ctx, q.listEntriesStmt, listEntries, arg.AccountID, arg.Limit, arg.Offset) 71 | if err != nil { 72 | return nil, err 73 | } 74 | defer rows.Close() 75 | items := []Entry{} 76 | for rows.Next() { 77 | var i Entry 78 | if err := rows.Scan( 79 | &i.ID, 80 | &i.AccountID, 81 | &i.Amount, 82 | &i.CreatedAt, 83 | ); err != nil { 84 | return nil, err 85 | } 86 | items = append(items, i) 87 | } 88 | if err := rows.Close(); err != nil { 89 | return nil, err 90 | } 91 | if err := rows.Err(); err != nil { 92 | return nil, err 93 | } 94 | return items, nil 95 | } 96 | -------------------------------------------------------------------------------- /api/transfer.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | 9 | db "github.com/CeciliaChen/simplebank/db/sqlc" 10 | "github.com/CeciliaChen/simplebank/token" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type transferRequest struct { 15 | FromAccountID int64 `json:"from_account_id" binding:"required,min=1"` 16 | ToAccountID int64 `json:"to_account_id" binding:"required,min=1"` 17 | Amount int64 `json:"amount" binding:"required,gt=0"` 18 | Currency string `json:"currency" binding:"required,currency"` 19 | } 20 | 21 | func (server *Server) createTransfer(ctx *gin.Context) { 22 | var req transferRequest 23 | if err := ctx.ShouldBindJSON(&req); err != nil { 24 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 25 | return 26 | } 27 | 28 | fromAccount, valid := server.validAccount(ctx, req.FromAccountID, req.Currency) 29 | if !valid { 30 | return 31 | } 32 | 33 | authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) 34 | if fromAccount.Owner != authPayload.Username { 35 | err := errors.New("from account doesn't belong to the authenticated user") 36 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 37 | return 38 | } 39 | 40 | _, valid = server.validAccount(ctx, req.ToAccountID, req.Currency) 41 | if !valid { 42 | return 43 | } 44 | 45 | arg := db.TransferTxParams{ 46 | FromAccountID: req.FromAccountID, 47 | ToAccountID: req.ToAccountID, 48 | Amount: req.Amount, 49 | } 50 | 51 | result, err := server.store.TransferTx(ctx, arg) 52 | if err != nil { 53 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 54 | return 55 | } 56 | 57 | ctx.JSON(http.StatusOK, result) 58 | } 59 | 60 | func (server *Server) validAccount(ctx *gin.Context, accountID int64, currency string) (db.Account, bool) { 61 | account, err := server.store.GetAccount(ctx, accountID) 62 | if err != nil { 63 | if err == sql.ErrNoRows { 64 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 65 | return account, false 66 | } 67 | 68 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 69 | return account, false 70 | } 71 | 72 | if account.Currency != currency { 73 | err := fmt.Errorf("account [%d] currency mismatch %s vs %s", account.ID, account.Currency, currency) 74 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 75 | return account, false 76 | } 77 | 78 | return account, true 79 | } 80 | -------------------------------------------------------------------------------- /db/sqlc/transfer_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/CeciliaChen/simplebank/util" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func createRandomTransfer(t *testing.T, account1, account2 Account) Transfer { 13 | arg := CreateTransferParams{ 14 | FromAccountID: account1.ID, 15 | ToAccountID: account2.ID, 16 | Amount: util.RandomMoney(), 17 | } 18 | 19 | transfer, err := testQueries.CreateTransfer(context.Background(), arg) 20 | require.NoError(t, err) 21 | require.NotEmpty(t, transfer) 22 | 23 | require.Equal(t, arg.FromAccountID, transfer.FromAccountID) 24 | require.Equal(t, arg.ToAccountID, transfer.ToAccountID) 25 | require.Equal(t, arg.Amount, transfer.Amount) 26 | 27 | require.NotZero(t, transfer.ID) 28 | require.NotZero(t, transfer.CreatedAt) 29 | 30 | return transfer 31 | } 32 | 33 | func TestCreateTransfer(t *testing.T) { 34 | account1 := createRandomAccount(t) 35 | account2 := createRandomAccount(t) 36 | createRandomTransfer(t, account1, account2) 37 | } 38 | 39 | func TestGetTransfer(t *testing.T) { 40 | account1 := createRandomAccount(t) 41 | account2 := createRandomAccount(t) 42 | transfer1 := createRandomTransfer(t, account1, account2) 43 | 44 | transfer2, err := testQueries.GetTransfer(context.Background(), transfer1.ID) 45 | require.NoError(t, err) 46 | require.NotEmpty(t, transfer2) 47 | 48 | require.Equal(t, transfer1.ID, transfer2.ID) 49 | require.Equal(t, transfer1.FromAccountID, transfer2.FromAccountID) 50 | require.Equal(t, transfer1.ToAccountID, transfer2.ToAccountID) 51 | require.Equal(t, transfer1.Amount, transfer2.Amount) 52 | require.WithinDuration(t, transfer1.CreatedAt, transfer2.CreatedAt, time.Second) 53 | } 54 | 55 | func TestListTransfer(t *testing.T) { 56 | account1 := createRandomAccount(t) 57 | account2 := createRandomAccount(t) 58 | 59 | for i := 0; i < 5; i++ { 60 | createRandomTransfer(t, account1, account2) 61 | createRandomTransfer(t, account2, account1) 62 | } 63 | 64 | arg := ListTransfersParams{ 65 | FromAccountID: account1.ID, 66 | ToAccountID: account1.ID, 67 | Limit: 5, 68 | Offset: 5, 69 | } 70 | 71 | transfers, err := testQueries.ListTransfers(context.Background(), arg) 72 | require.NoError(t, err) 73 | require.Len(t, transfers, 5) 74 | 75 | for _, transfer := range transfers { 76 | require.NotEmpty(t, transfer) 77 | require.True(t, transfer.FromAccountID == account1.ID || transfer.ToAccountID == account1.ID) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/CeciliaChen/simplebank 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/go-playground/validator/v10 v10.15.4 9 | github.com/golang/mock v1.6.0 10 | github.com/google/uuid v1.3.1 11 | github.com/lib/pq v1.10.9 12 | github.com/spf13/viper v1.16.0 13 | github.com/stretchr/testify v1.8.4 14 | golang.org/x/crypto v0.13.0 15 | ) 16 | 17 | require ( 18 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect 19 | github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb // indirect 20 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect 21 | github.com/bytedance/sonic v1.10.1 // indirect 22 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 23 | github.com/chenzhuoyu/iasm v0.9.0 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/fsnotify/fsnotify v1.6.0 // indirect 26 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 27 | github.com/gin-contrib/sse v0.1.0 // indirect 28 | github.com/go-playground/locales v0.14.1 // indirect 29 | github.com/go-playground/universal-translator v0.18.1 // indirect 30 | github.com/goccy/go-json v0.10.2 // indirect 31 | github.com/hashicorp/hcl v1.0.0 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 34 | github.com/leodido/go-urn v1.2.4 // indirect 35 | github.com/magiconair/properties v1.8.7 // indirect 36 | github.com/mattn/go-isatty v0.0.19 // indirect 37 | github.com/mitchellh/mapstructure v1.5.0 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/o1egl/paseto v1.0.0 // indirect 41 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 42 | github.com/pkg/errors v0.9.1 // indirect 43 | github.com/pmezard/go-difflib v1.0.0 // indirect 44 | github.com/spf13/afero v1.9.5 // indirect 45 | github.com/spf13/cast v1.5.1 // indirect 46 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 47 | github.com/spf13/pflag v1.0.5 // indirect 48 | github.com/subosito/gotenv v1.4.2 // indirect 49 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 50 | github.com/ugorji/go/codec v1.2.11 // indirect 51 | golang.org/x/arch v0.5.0 // indirect 52 | golang.org/x/net v0.15.0 // indirect 53 | golang.org/x/sys v0.12.0 // indirect 54 | golang.org/x/text v0.13.0 // indirect 55 | google.golang.org/protobuf v1.31.0 // indirect 56 | gopkg.in/ini.v1 v1.67.0 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /db/sqlc/transfer.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.22.0 4 | // source: transfer.sql 5 | 6 | package db 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const createTransfer = `-- name: CreateTransfer :one 13 | INSERT INTO transfers ( 14 | from_account_id, 15 | to_account_id, 16 | amount 17 | ) VALUES ( 18 | $1, $2, $3 19 | ) RETURNING id, from_account_id, to_account_id, amount, created_at 20 | ` 21 | 22 | type CreateTransferParams struct { 23 | FromAccountID int64 `json:"from_account_id"` 24 | ToAccountID int64 `json:"to_account_id"` 25 | Amount int64 `json:"amount"` 26 | } 27 | 28 | func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) (Transfer, error) { 29 | row := q.queryRow(ctx, q.createTransferStmt, createTransfer, arg.FromAccountID, arg.ToAccountID, arg.Amount) 30 | var i Transfer 31 | err := row.Scan( 32 | &i.ID, 33 | &i.FromAccountID, 34 | &i.ToAccountID, 35 | &i.Amount, 36 | &i.CreatedAt, 37 | ) 38 | return i, err 39 | } 40 | 41 | const getTransfer = `-- name: GetTransfer :one 42 | SELECT id, from_account_id, to_account_id, amount, created_at FROM transfers 43 | WHERE id = $1 LIMIT 1 44 | ` 45 | 46 | func (q *Queries) GetTransfer(ctx context.Context, id int64) (Transfer, error) { 47 | row := q.queryRow(ctx, q.getTransferStmt, getTransfer, id) 48 | var i Transfer 49 | err := row.Scan( 50 | &i.ID, 51 | &i.FromAccountID, 52 | &i.ToAccountID, 53 | &i.Amount, 54 | &i.CreatedAt, 55 | ) 56 | return i, err 57 | } 58 | 59 | const listTransfers = `-- name: ListTransfers :many 60 | SELECT id, from_account_id, to_account_id, amount, created_at FROM transfers 61 | WHERE 62 | from_account_id = $1 OR 63 | to_account_id = $2 64 | ORDER BY id 65 | LIMIT $3 66 | OFFSET $4 67 | ` 68 | 69 | type ListTransfersParams struct { 70 | FromAccountID int64 `json:"from_account_id"` 71 | ToAccountID int64 `json:"to_account_id"` 72 | Limit int32 `json:"limit"` 73 | Offset int32 `json:"offset"` 74 | } 75 | 76 | func (q *Queries) ListTransfers(ctx context.Context, arg ListTransfersParams) ([]Transfer, error) { 77 | rows, err := q.query(ctx, q.listTransfersStmt, listTransfers, 78 | arg.FromAccountID, 79 | arg.ToAccountID, 80 | arg.Limit, 81 | arg.Offset, 82 | ) 83 | if err != nil { 84 | return nil, err 85 | } 86 | defer rows.Close() 87 | items := []Transfer{} 88 | for rows.Next() { 89 | var i Transfer 90 | if err := rows.Scan( 91 | &i.ID, 92 | &i.FromAccountID, 93 | &i.ToAccountID, 94 | &i.Amount, 95 | &i.CreatedAt, 96 | ); err != nil { 97 | return nil, err 98 | } 99 | items = append(items, i) 100 | } 101 | if err := rows.Close(); err != nil { 102 | return nil, err 103 | } 104 | if err := rows.Err(); err != nil { 105 | return nil, err 106 | } 107 | return items, nil 108 | } 109 | -------------------------------------------------------------------------------- /db/sqlc/account_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "testing" 7 | "time" 8 | 9 | "github.com/CeciliaChen/simplebank/util" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func createRandomAccount(t *testing.T) Account { 14 | user := createRandomUser(t) 15 | 16 | arg := CreateAccountParams{ 17 | Owner: user.Username, 18 | Balance: util.RandomMoney(), 19 | Currency: util.RandomCurrency(), 20 | } 21 | 22 | account, err := testQueries.CreateAccount(context.Background(), arg) 23 | require.NoError(t, err) 24 | require.NotEmpty(t, account) 25 | 26 | require.Equal(t, arg.Owner, account.Owner) 27 | require.Equal(t, arg.Balance, account.Balance) 28 | require.Equal(t, arg.Currency, account.Currency) 29 | 30 | require.NotZero(t, account.ID) 31 | require.NotZero(t, account.CreatedAt) 32 | 33 | return account 34 | } 35 | 36 | func TestCreateAccount(t *testing.T) { 37 | createRandomAccount(t) 38 | } 39 | 40 | func TestGetAccount(t *testing.T) { 41 | account1 := createRandomAccount(t) 42 | account2, err := testQueries.GetAccount(context.Background(), account1.ID) 43 | require.NoError(t, err) 44 | require.NotEmpty(t, account2) 45 | 46 | require.Equal(t, account1.ID, account2.ID) 47 | require.Equal(t, account1.Owner, account2.Owner) 48 | require.Equal(t, account1.Balance, account2.Balance) 49 | require.Equal(t, account1.Currency, account2.Currency) 50 | require.WithinDuration(t, account1.CreatedAt, account2.CreatedAt, time.Second) 51 | } 52 | 53 | func TestUpdateAccount(t *testing.T) { 54 | account1 := createRandomAccount(t) 55 | arg := UpdateAccountParams{ 56 | ID: account1.ID, 57 | Balance: util.RandomMoney(), 58 | } 59 | 60 | account2, err := testQueries.UpdateAccount(context.Background(), arg) 61 | require.NoError(t, err) 62 | require.NotEmpty(t, account2) 63 | 64 | require.Equal(t, account1.ID, account2.ID) 65 | require.Equal(t, account1.Owner, account2.Owner) 66 | require.Equal(t, arg.Balance, account2.Balance) 67 | require.Equal(t, account1.Currency, account2.Currency) 68 | require.WithinDuration(t, account1.CreatedAt, account2.CreatedAt, time.Second) 69 | } 70 | 71 | func TestDeleteAccount(t *testing.T) { 72 | account1 := createRandomAccount(t) 73 | err := testQueries.DeleteAccount(context.Background(), account1.ID) 74 | require.NoError(t, err) 75 | 76 | account2, err := testQueries.GetAccount(context.Background(), account1.ID) 77 | require.Error(t, err) 78 | require.EqualError(t, err, sql.ErrNoRows.Error()) 79 | require.Empty(t, account2) 80 | } 81 | 82 | func TestListAccounts(t *testing.T) { 83 | var lastAccount Account 84 | for i := 0; i < 10; i++ { 85 | lastAccount = createRandomAccount(t) 86 | } 87 | 88 | arg := ListAccountsParams{ 89 | Owner: lastAccount.Owner, 90 | Limit: 5, 91 | Offset: 0, 92 | } 93 | 94 | accounts, err := testQueries.ListAccounts(context.Background(), arg) 95 | require.NoError(t, err) 96 | require.NotEmpty(t, accounts) 97 | 98 | for _, account := range accounts { 99 | require.NotEmpty(t, account) 100 | require.Equal(t, lastAccount.Owner, account.Owner) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /db/sqlc/store.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | ) 8 | 9 | // Store provides all functions to execute db queries and transaction 10 | type Store interface { 11 | Querier 12 | TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) 13 | } 14 | 15 | // SQLStore provides all functions to execute SQL queries and transaction 16 | type SQLStore struct { 17 | *Queries 18 | db *sql.DB 19 | } 20 | 21 | func NewStore(db *sql.DB) Store { 22 | return &SQLStore{ 23 | db: db, 24 | Queries: New(db), 25 | } 26 | } 27 | 28 | // execTx executes a function within a database transaction 29 | func (store *SQLStore) execTx(ctx context.Context, fn func(*Queries) error) error { 30 | tx, err := store.db.BeginTx(ctx, nil) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | q := New(tx) 36 | err = fn(q) 37 | if err != nil { 38 | if rbErr := tx.Rollback(); rbErr != nil { 39 | return fmt.Errorf("tx err: %v, rb err: %v", err, rbErr) 40 | } 41 | return err 42 | } 43 | return tx.Commit() 44 | } 45 | 46 | type TransferTxParams struct { 47 | FromAccountID int64 `json:"from_account_id"` 48 | ToAccountID int64 `json:"to_account_id"` 49 | Amount int64 `json:"amount"` 50 | } 51 | 52 | type TransferTxResult struct { 53 | Transfer Transfer `json:"transfer"` 54 | FromAccount Account `json:"from_account"` 55 | ToAccount Account `json:"to_account"` 56 | FromEntry Entry `json:"from_entry"` 57 | ToEntry Entry `json:"to_entry"` 58 | } 59 | 60 | // TransferTx performs a money transfer from one account to the other 61 | func (store *SQLStore) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) { 62 | var result TransferTxResult 63 | 64 | err := store.execTx(ctx, func(q *Queries) error { 65 | var err error 66 | 67 | result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{ 68 | FromAccountID: arg.FromAccountID, 69 | ToAccountID: arg.ToAccountID, 70 | Amount: arg.Amount, 71 | }) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{ 77 | AccountID: arg.FromAccountID, 78 | Amount: -arg.Amount, 79 | }) 80 | 81 | if err != nil { 82 | return err 83 | } 84 | 85 | result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{ 86 | AccountID: arg.ToAccountID, 87 | Amount: arg.Amount, 88 | }) 89 | 90 | if err != nil { 91 | return err 92 | } 93 | 94 | if arg.FromAccountID < arg.ToAccountID { 95 | result.FromAccount, result.ToAccount, err = addMoney(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount) 96 | 97 | } else { 98 | result.ToAccount, result.FromAccount, err = addMoney(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount) 99 | 100 | } 101 | 102 | return nil 103 | }) 104 | 105 | return result, err 106 | } 107 | 108 | func addMoney( 109 | ctx context.Context, 110 | q *Queries, 111 | accountID1 int64, 112 | amount1 int64, 113 | accountID2 int64, 114 | amount2 int64, 115 | ) (account1 Account, account2 Account, err error) { 116 | account1, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ 117 | ID: accountID1, 118 | Amount: amount1, 119 | }) 120 | if err != nil { 121 | return 122 | } 123 | 124 | account2, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ 125 | ID: accountID2, 126 | Amount: amount2, 127 | }) 128 | 129 | return 130 | } 131 | -------------------------------------------------------------------------------- /api/middleware_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "net/http" 9 | "net/http/httptest" 10 | 11 | "github.com/CeciliaChen/simplebank/token" 12 | "github.com/gin-gonic/gin" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func addAuthorization( 17 | t *testing.T, 18 | request *http.Request, 19 | tokenMaker token.Maker, 20 | authorizationType string, 21 | username string, 22 | duration time.Duration, 23 | ) { 24 | token, err := tokenMaker.CreateToken(username, duration) 25 | require.NoError(t, err) 26 | 27 | authorizationHeader := fmt.Sprintf("%s %s", authorizationType, token) 28 | request.Header.Set(authorizationHeaderKey, authorizationHeader) 29 | } 30 | 31 | func TestAuthMiddleware(t *testing.T) { 32 | testCases := []struct { 33 | name string 34 | setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker) 35 | checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder) 36 | }{ 37 | { 38 | name: "OK", 39 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 40 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", time.Minute) 41 | }, 42 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 43 | require.Equal(t, http.StatusOK, recorder.Code) 44 | }, 45 | }, 46 | { 47 | name: "No Authorization", 48 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 49 | }, 50 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 51 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 52 | }, 53 | }, 54 | { 55 | name: "UnsupportedAuthorization", 56 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 57 | addAuthorization(t, request, tokenMaker, "unsupported", "user", time.Minute) 58 | }, 59 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 60 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 61 | }, 62 | }, 63 | { 64 | name: "InvalidAuthorizationFormat", 65 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 66 | addAuthorization(t, request, tokenMaker, "", "user", time.Minute) 67 | }, 68 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 69 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 70 | }, 71 | }, 72 | { 73 | name: "ExpiredToken", 74 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 75 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", -time.Minute) 76 | }, 77 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 78 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 79 | }, 80 | }, 81 | } 82 | 83 | for _, tc := range testCases { 84 | t.Run(tc.name, func(t *testing.T) { 85 | server := newTestServer(t, nil) 86 | 87 | authPath := "/auth" 88 | server.router.GET( 89 | authPath, 90 | authMiddleware(server.tokenMaker), 91 | func(ctx *gin.Context) { 92 | ctx.JSON(http.StatusOK, gin.H{}) 93 | }, 94 | ) 95 | 96 | recorder := httptest.NewRecorder() 97 | request, err := http.NewRequest(http.MethodGet, authPath, nil) 98 | require.NoError(t, err) 99 | 100 | tc.setupAuth(t, request, server.tokenMaker) 101 | server.router.ServeHTTP(recorder, request) 102 | tc.checkResponse(t, recorder) 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /api/user.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "net/http" 7 | 8 | "time" 9 | 10 | db "github.com/CeciliaChen/simplebank/db/sqlc" 11 | "github.com/CeciliaChen/simplebank/util" 12 | "github.com/gin-gonic/gin" 13 | "github.com/lib/pq" 14 | ) 15 | 16 | type createUserRequest struct { 17 | Username string `json:"username" binding:"required,alphanum"` 18 | Password string `json:"password" binding:"required,min=6"` 19 | FullName string `json:"full_name" binding:"required"` 20 | Email string `json:"email" binding:"required,email"` 21 | } 22 | 23 | type userResponse struct { 24 | Username string `json:"username"` 25 | FullName string `json:"full_name"` 26 | Email string `json:"email"` 27 | PasswordChangedAt time.Time `json:"password_changed_at"` 28 | CreatedAt time.Time `json:"created_at"` 29 | } 30 | 31 | func newUserResponse(user db.User) userResponse { 32 | return userResponse{ 33 | Username: user.Username, 34 | FullName: user.FullName, 35 | Email: user.Email, 36 | PasswordChangedAt: user.PasswordChangedAt, 37 | CreatedAt: user.CreatedAt, 38 | } 39 | } 40 | 41 | func (server *Server) createUser(ctx *gin.Context) { 42 | var req createUserRequest 43 | if err := ctx.ShouldBindJSON(&req); err != nil { 44 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 45 | return 46 | } 47 | 48 | hashedPassword, err := util.HashPassword(req.Password) 49 | if err != nil { 50 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 51 | return 52 | } 53 | 54 | arg := db.CreateUserParams{ 55 | Username: req.Username, 56 | HashedPassword: hashedPassword, 57 | FullName: req.FullName, 58 | Email: req.Email, 59 | } 60 | 61 | user, err := server.store.CreateUser(ctx, arg) 62 | if err != nil { 63 | if pqErr, ok := err.(*pq.Error); ok { 64 | log.Println(pqErr.Code.Name()) 65 | switch pqErr.Code.Name() { 66 | case "unique_violation": 67 | ctx.JSON(http.StatusForbidden, errorResponse(err)) 68 | return 69 | } 70 | } 71 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 72 | return 73 | } 74 | 75 | rsp := newUserResponse(user) 76 | ctx.JSON(http.StatusOK, rsp) 77 | } 78 | 79 | type loginUserRequest struct { 80 | Username string `json:"username" binding:"required,alphanum"` 81 | Password string `json:"password" binding:"required,min=6"` 82 | } 83 | 84 | type loginUserResponse struct { 85 | AccessToken string `json:"access_token"` 86 | User userResponse `json:"user"` 87 | } 88 | 89 | func (server *Server) loginUser(ctx *gin.Context) { 90 | var req loginUserRequest 91 | if err := ctx.ShouldBindJSON(&req); err != nil { 92 | ctx.JSON(http.StatusBadRequest, errorResponse((err))) 93 | return 94 | } 95 | 96 | user, err := server.store.GetUser(ctx, req.Username) 97 | if err != nil { 98 | if err == sql.ErrNoRows { 99 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 100 | return 101 | } 102 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 103 | return 104 | } 105 | 106 | err = util.CheckPassword(req.Password, user.HashedPassword) 107 | if err != nil { 108 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 109 | return 110 | } 111 | 112 | accessToken, err := server.tokenMaker.CreateToken( 113 | user.Username, 114 | server.config.AccessTokenDuration, 115 | ) 116 | if err != nil { 117 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 118 | return 119 | } 120 | 121 | rsp := loginUserResponse{ 122 | AccessToken: accessToken, 123 | User: newUserResponse(user), 124 | } 125 | ctx.JSON(http.StatusOK, rsp) 126 | } 127 | -------------------------------------------------------------------------------- /db/sqlc/account.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.22.0 4 | // source: account.sql 5 | 6 | package db 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const addAccountBalance = `-- name: AddAccountBalance :one 13 | UPDATE accounts 14 | SET balance = balance + $1 15 | WHERE id = $2 16 | RETURNING id, owner, balance, currency, created_at 17 | ` 18 | 19 | type AddAccountBalanceParams struct { 20 | Amount int64 `json:"amount"` 21 | ID int64 `json:"id"` 22 | } 23 | 24 | func (q *Queries) AddAccountBalance(ctx context.Context, arg AddAccountBalanceParams) (Account, error) { 25 | row := q.queryRow(ctx, q.addAccountBalanceStmt, addAccountBalance, arg.Amount, arg.ID) 26 | var i Account 27 | err := row.Scan( 28 | &i.ID, 29 | &i.Owner, 30 | &i.Balance, 31 | &i.Currency, 32 | &i.CreatedAt, 33 | ) 34 | return i, err 35 | } 36 | 37 | const createAccount = `-- name: CreateAccount :one 38 | INSERT INTO accounts ( 39 | owner, 40 | balance, 41 | currency 42 | ) VALUES ( 43 | $1, $2, $3 44 | ) RETURNING id, owner, balance, currency, created_at 45 | ` 46 | 47 | type CreateAccountParams struct { 48 | Owner string `json:"owner"` 49 | Balance int64 `json:"balance"` 50 | Currency string `json:"currency"` 51 | } 52 | 53 | func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) { 54 | row := q.queryRow(ctx, q.createAccountStmt, createAccount, arg.Owner, arg.Balance, arg.Currency) 55 | var i Account 56 | err := row.Scan( 57 | &i.ID, 58 | &i.Owner, 59 | &i.Balance, 60 | &i.Currency, 61 | &i.CreatedAt, 62 | ) 63 | return i, err 64 | } 65 | 66 | const deleteAccount = `-- name: DeleteAccount :exec 67 | DELETE FROM accounts 68 | WHERE id = $1 69 | ` 70 | 71 | func (q *Queries) DeleteAccount(ctx context.Context, id int64) error { 72 | _, err := q.exec(ctx, q.deleteAccountStmt, deleteAccount, id) 73 | return err 74 | } 75 | 76 | const getAccount = `-- name: GetAccount :one 77 | SELECT id, owner, balance, currency, created_at FROM accounts 78 | WHERE id = $1 LIMIT 1 79 | ` 80 | 81 | func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) { 82 | row := q.queryRow(ctx, q.getAccountStmt, getAccount, id) 83 | var i Account 84 | err := row.Scan( 85 | &i.ID, 86 | &i.Owner, 87 | &i.Balance, 88 | &i.Currency, 89 | &i.CreatedAt, 90 | ) 91 | return i, err 92 | } 93 | 94 | const getAccountForUpdate = `-- name: GetAccountForUpdate :one 95 | SELECT id, owner, balance, currency, created_at FROM accounts 96 | WHERE id = $1 LIMIT 1 97 | FOR NO KEY UPDATE 98 | ` 99 | 100 | func (q *Queries) GetAccountForUpdate(ctx context.Context, id int64) (Account, error) { 101 | row := q.queryRow(ctx, q.getAccountForUpdateStmt, getAccountForUpdate, id) 102 | var i Account 103 | err := row.Scan( 104 | &i.ID, 105 | &i.Owner, 106 | &i.Balance, 107 | &i.Currency, 108 | &i.CreatedAt, 109 | ) 110 | return i, err 111 | } 112 | 113 | const listAccounts = `-- name: ListAccounts :many 114 | SELECT id, owner, balance, currency, created_at FROM accounts 115 | WHERE owner = $1 116 | ORDER BY id 117 | LIMIT $2 118 | OFFSET $3 119 | ` 120 | 121 | type ListAccountsParams struct { 122 | Owner string `json:"owner"` 123 | Limit int32 `json:"limit"` 124 | Offset int32 `json:"offset"` 125 | } 126 | 127 | func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) { 128 | rows, err := q.query(ctx, q.listAccountsStmt, listAccounts, arg.Owner, arg.Limit, arg.Offset) 129 | if err != nil { 130 | return nil, err 131 | } 132 | defer rows.Close() 133 | items := []Account{} 134 | for rows.Next() { 135 | var i Account 136 | if err := rows.Scan( 137 | &i.ID, 138 | &i.Owner, 139 | &i.Balance, 140 | &i.Currency, 141 | &i.CreatedAt, 142 | ); err != nil { 143 | return nil, err 144 | } 145 | items = append(items, i) 146 | } 147 | if err := rows.Close(); err != nil { 148 | return nil, err 149 | } 150 | if err := rows.Err(); err != nil { 151 | return nil, err 152 | } 153 | return items, nil 154 | } 155 | 156 | const updateAccount = `-- name: UpdateAccount :one 157 | UPDATE accounts 158 | SET balance = $2 159 | WHERE id = $1 160 | RETURNING id, owner, balance, currency, created_at 161 | ` 162 | 163 | type UpdateAccountParams struct { 164 | ID int64 `json:"id"` 165 | Balance int64 `json:"balance"` 166 | } 167 | 168 | func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) { 169 | row := q.queryRow(ctx, q.updateAccountStmt, updateAccount, arg.ID, arg.Balance) 170 | var i Account 171 | err := row.Scan( 172 | &i.ID, 173 | &i.Owner, 174 | &i.Balance, 175 | &i.Currency, 176 | &i.CreatedAt, 177 | ) 178 | return i, err 179 | } 180 | -------------------------------------------------------------------------------- /wait-for.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2017 Eficode Oy 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | VERSION="2.2.4" 26 | 27 | set -- "$@" -- "$TIMEOUT" "$QUIET" "$PROTOCOL" "$HOST" "$PORT" "$result" 28 | TIMEOUT=15 29 | QUIET=0 30 | # The protocol to make the request with, either "tcp" or "http" 31 | PROTOCOL="tcp" 32 | 33 | echoerr() { 34 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi 35 | } 36 | 37 | usage() { 38 | exitcode="$1" 39 | cat << USAGE >&2 40 | Usage: 41 | $0 host:port|url [-t timeout] [-- command args] 42 | -q | --quiet Do not output any status messages 43 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout 44 | Defaults to 15 seconds 45 | -v | --version Show the version of this tool 46 | -- COMMAND ARGS Execute command with args after the test finishes 47 | USAGE 48 | exit "$exitcode" 49 | } 50 | 51 | wait_for() { 52 | case "$PROTOCOL" in 53 | tcp) 54 | if ! command -v nc >/dev/null; then 55 | echoerr 'nc command is missing!' 56 | exit 1 57 | fi 58 | ;; 59 | http) 60 | if ! command -v wget >/dev/null; then 61 | echoerr 'wget command is missing!' 62 | exit 1 63 | fi 64 | ;; 65 | esac 66 | 67 | TIMEOUT_END=$(($(date +%s) + TIMEOUT)) 68 | 69 | while :; do 70 | case "$PROTOCOL" in 71 | tcp) 72 | nc -w 1 -z "$HOST" "$PORT" > /dev/null 2>&1 73 | ;; 74 | http) 75 | wget --timeout=1 --tries=1 -q "$HOST" -O /dev/null > /dev/null 2>&1 76 | ;; 77 | *) 78 | echoerr "Unknown protocol '$PROTOCOL'" 79 | exit 1 80 | ;; 81 | esac 82 | 83 | result=$? 84 | 85 | if [ $result -eq 0 ] ; then 86 | if [ $# -gt 7 ] ; then 87 | for result in $(seq $(($# - 7))); do 88 | result=$1 89 | shift 90 | set -- "$@" "$result" 91 | done 92 | 93 | TIMEOUT=$2 QUIET=$3 PROTOCOL=$4 HOST=$5 PORT=$6 result=$7 94 | shift 7 95 | exec "$@" 96 | fi 97 | exit 0 98 | fi 99 | 100 | if [ $TIMEOUT -ne 0 -a $(date +%s) -ge $TIMEOUT_END ]; then 101 | echo "Operation timed out" >&2 102 | exit 1 103 | fi 104 | 105 | sleep 1 106 | done 107 | } 108 | 109 | while :; do 110 | case "$1" in 111 | http://*|https://*) 112 | HOST="$1" 113 | PROTOCOL="http" 114 | shift 1 115 | ;; 116 | *:* ) 117 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1) 118 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2) 119 | shift 1 120 | ;; 121 | -v | --version) 122 | echo $VERSION 123 | exit 124 | ;; 125 | -q | --quiet) 126 | QUIET=1 127 | shift 1 128 | ;; 129 | -q-*) 130 | QUIET=0 131 | echoerr "Unknown option: $1" 132 | usage 1 133 | ;; 134 | -q*) 135 | QUIET=1 136 | result=$1 137 | shift 1 138 | set -- -"${result#-q}" "$@" 139 | ;; 140 | -t | --timeout) 141 | TIMEOUT="$2" 142 | shift 2 143 | ;; 144 | -t*) 145 | TIMEOUT="${1#-t}" 146 | shift 1 147 | ;; 148 | --timeout=*) 149 | TIMEOUT="${1#*=}" 150 | shift 1 151 | ;; 152 | --) 153 | shift 154 | break 155 | ;; 156 | --help) 157 | usage 0 158 | ;; 159 | -*) 160 | QUIET=0 161 | echoerr "Unknown option: $1" 162 | usage 1 163 | ;; 164 | *) 165 | QUIET=0 166 | echoerr "Unknown argument: $1" 167 | usage 1 168 | ;; 169 | esac 170 | done 171 | 172 | if ! [ "$TIMEOUT" -ge 0 ] 2>/dev/null; then 173 | echoerr "Error: invalid timeout '$TIMEOUT'" 174 | usage 3 175 | fi 176 | 177 | case "$PROTOCOL" in 178 | tcp) 179 | if [ "$HOST" = "" ] || [ "$PORT" = "" ]; then 180 | echoerr "Error: you need to provide a host and port to test." 181 | usage 2 182 | fi 183 | ;; 184 | http) 185 | if [ "$HOST" = "" ]; then 186 | echoerr "Error: you need to provide a host to test." 187 | usage 2 188 | fi 189 | ;; 190 | esac 191 | 192 | wait_for "$@" 193 | -------------------------------------------------------------------------------- /db/sqlc/store_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTransferTx(t *testing.T) { 12 | store := NewStore(testDB) 13 | 14 | account1 := createRandomAccount(t) 15 | account2 := createRandomAccount(t) 16 | fmt.Println(">> before:", account1.Balance, account2.Balance) 17 | 18 | // run n concurrent transfer transactions 19 | n := 5 20 | amount := int64(10) 21 | 22 | errs := make(chan error) 23 | results := make(chan TransferTxResult) 24 | 25 | for i := 0; i < n; i++ { 26 | go func() { 27 | ctx := context.Background() 28 | result, err := store.TransferTx(ctx, TransferTxParams{ 29 | FromAccountID: account1.ID, 30 | ToAccountID: account2.ID, 31 | Amount: amount, 32 | }) 33 | 34 | errs <- err // Fixed here 35 | results <- result 36 | }() 37 | } 38 | 39 | //check results 40 | existed := make(map[int]bool) 41 | 42 | for i := 0; i < n; i++ { 43 | err := <-errs 44 | require.NoError(t, err) 45 | 46 | result := <-results 47 | require.NotEmpty(t, result) 48 | 49 | //check transfer 50 | transfer := result.Transfer 51 | require.NotEmpty(t, transfer) 52 | require.Equal(t, account1.ID, transfer.FromAccountID) 53 | require.Equal(t, account2.ID, transfer.ToAccountID) 54 | require.Equal(t, amount, transfer.Amount) 55 | require.NotZero(t, transfer.ID) 56 | require.NotZero(t, transfer.CreatedAt) 57 | 58 | _, err = store.GetTransfer(context.Background(), transfer.ID) 59 | require.NoError(t, err) 60 | 61 | //check entries 62 | fromEntry := result.FromEntry 63 | require.NotEmpty(t, fromEntry) 64 | require.Equal(t, account1.ID, fromEntry.AccountID) 65 | require.Equal(t, -amount, fromEntry.Amount) 66 | require.NotZero(t, fromEntry.ID) 67 | require.NotZero(t, fromEntry.CreatedAt) 68 | 69 | _, err = store.GetEntry(context.Background(), fromEntry.ID) 70 | require.NoError(t, err) 71 | 72 | toEntry := result.ToEntry 73 | require.NotEmpty(t, toEntry) 74 | require.Equal(t, account2.ID, toEntry.AccountID) 75 | require.Equal(t, amount, toEntry.Amount) 76 | require.NotZero(t, toEntry.ID) 77 | require.NotZero(t, toEntry.CreatedAt) 78 | 79 | _, err = store.GetEntry(context.Background(), toEntry.ID) 80 | require.NoError(t, err) 81 | 82 | //check accounts 83 | fromAccount := result.FromAccount 84 | require.NotEmpty(t, fromAccount) 85 | require.Equal(t, account1.ID, fromAccount.ID) 86 | 87 | toAccount := result.ToAccount 88 | require.NotEmpty(t, toAccount) 89 | require.Equal(t, account2.ID, toAccount.ID) 90 | 91 | //check accounts' balance 92 | fmt.Println(">> tx:", fromAccount.Balance, toAccount.Balance) 93 | 94 | diff1 := account1.Balance - fromAccount.Balance 95 | diff2 := toAccount.Balance - account2.Balance 96 | require.Equal(t, diff1, diff2) 97 | require.True(t, diff1 > 0) 98 | require.True(t, diff1%amount == 0) //amount, 2*amount, 3*amount, ..., n * amount 99 | 100 | k := int(diff1 / amount) 101 | require.True(t, k >= 1 && k <= n) 102 | require.NotContains(t, existed, k) 103 | existed[k] = true 104 | } 105 | 106 | // check the final updated balances 107 | updatedAccount1, err := testQueries.GetAccount(context.Background(), account1.ID) 108 | require.NoError(t, err) 109 | require.NotEmpty(t, updatedAccount1) 110 | 111 | updatedAccount2, err := testQueries.GetAccount(context.Background(), account2.ID) 112 | require.NoError(t, err) 113 | require.NotEmpty(t, updatedAccount2) 114 | 115 | fmt.Println(">> before:", updatedAccount1.Balance, updatedAccount2.Balance) 116 | 117 | require.Equal(t, account1.Balance-int64(n)*amount, updatedAccount1.Balance) 118 | require.Equal(t, account2.Balance+int64(n)*amount, updatedAccount2.Balance) 119 | } 120 | 121 | func TestTransferTxDeadlock(t *testing.T) { 122 | store := NewStore(testDB) 123 | 124 | account1 := createRandomAccount(t) 125 | account2 := createRandomAccount(t) 126 | fmt.Println(">> before:", account1.Balance, account2.Balance) 127 | 128 | // run n concurrent transfer transactions 129 | n := 10 130 | amount := int64(10) 131 | errs := make(chan error) 132 | 133 | for i := 0; i < n; i++ { 134 | fromAccountID := account1.ID 135 | toAccountID := account2.ID 136 | 137 | if i%2 == 1 { 138 | fromAccountID = account2.ID 139 | toAccountID = account1.ID 140 | } 141 | 142 | go func() { 143 | ctx := context.Background() 144 | _, err := store.TransferTx(ctx, TransferTxParams{ 145 | FromAccountID: fromAccountID, 146 | ToAccountID: toAccountID, 147 | Amount: amount, 148 | }) 149 | 150 | errs <- err 151 | }() 152 | } 153 | 154 | for i := 0; i < n; i++ { 155 | err := <-errs 156 | require.NoError(t, err) 157 | } 158 | 159 | // check the final updated balances 160 | updatedAccount1, err := store.GetAccount(context.Background(), account1.ID) 161 | require.NoError(t, err) 162 | 163 | updatedAccount2, err := store.GetAccount(context.Background(), account2.ID) 164 | require.NoError(t, err) 165 | 166 | fmt.Println(">> before:", updatedAccount1.Balance, updatedAccount2.Balance) 167 | 168 | require.Equal(t, account1.Balance, updatedAccount1.Balance) 169 | require.Equal(t, account2.Balance, updatedAccount2.Balance) 170 | } 171 | -------------------------------------------------------------------------------- /api/account_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | "time" 13 | 14 | mockdb "github.com/CeciliaChen/simplebank/db/mock" 15 | db "github.com/CeciliaChen/simplebank/db/sqlc" 16 | "github.com/CeciliaChen/simplebank/token" 17 | "github.com/CeciliaChen/simplebank/util" 18 | "github.com/golang/mock/gomock" 19 | "github.com/stretchr/testify/require" 20 | ) 21 | 22 | func TestGetAccountApi(t *testing.T) { 23 | user, _ := randomUser(t) 24 | account := randomAccount(user.Username) 25 | 26 | testCases := []struct { 27 | name string 28 | accountID int64 29 | setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker) 30 | buildStubs func(store *mockdb.MockStore) 31 | checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder) 32 | }{ 33 | { 34 | name: "OK", 35 | accountID: account.ID, 36 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 37 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) 38 | }, 39 | buildStubs: func(store *mockdb.MockStore) { 40 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(account, nil) 41 | }, 42 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 43 | require.Equal(t, http.StatusOK, recorder.Code) 44 | requiredBodyMatchAccount(t, recorder.Body, account) 45 | }, 46 | }, 47 | { 48 | name: "UnauthorizedUser", 49 | accountID: account.ID, 50 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 51 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "unauthorized_user", time.Minute) 52 | }, 53 | buildStubs: func(store *mockdb.MockStore) { 54 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(account, nil) 55 | }, 56 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 57 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 58 | }, 59 | }, 60 | { 61 | name: "NoAuthorization", 62 | accountID: account.ID, 63 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 64 | }, 65 | buildStubs: func(store *mockdb.MockStore) { 66 | store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) 67 | }, 68 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 69 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 70 | }, 71 | }, 72 | { 73 | name: "NOTFOUND", 74 | accountID: account.ID, 75 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 76 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) 77 | }, 78 | buildStubs: func(store *mockdb.MockStore) { 79 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(db.Account{}, sql.ErrNoRows) 80 | }, 81 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 82 | require.Equal(t, http.StatusNotFound, recorder.Code) 83 | }, 84 | }, 85 | { 86 | name: "InternalError", 87 | accountID: account.ID, 88 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 89 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) 90 | }, 91 | buildStubs: func(store *mockdb.MockStore) { 92 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(db.Account{}, sql.ErrConnDone) 93 | }, 94 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 95 | require.Equal(t, http.StatusInternalServerError, recorder.Code) 96 | }, 97 | }, 98 | { 99 | name: "InvalidID", 100 | accountID: 0, 101 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 102 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) 103 | }, 104 | buildStubs: func(store *mockdb.MockStore) { 105 | store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) 106 | }, 107 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 108 | require.Equal(t, http.StatusBadRequest, recorder.Code) 109 | }, 110 | }, 111 | } 112 | 113 | for i := range testCases { 114 | tc := testCases[i] 115 | 116 | t.Run(tc.name, func(t *testing.T) { 117 | ctrl := gomock.NewController(t) 118 | defer ctrl.Finish() 119 | 120 | store := mockdb.NewMockStore(ctrl) 121 | tc.buildStubs(store) 122 | 123 | // start test server and send request 124 | server := newTestServer(t, store) 125 | recorder := httptest.NewRecorder() 126 | 127 | url := fmt.Sprintf("/accounts/%d", tc.accountID) 128 | request, err := http.NewRequest(http.MethodGet, url, nil) 129 | require.NoError(t, err) 130 | 131 | tc.setupAuth(t, request, server.tokenMaker) 132 | server.router.ServeHTTP(recorder, request) 133 | tc.checkResponse(t, recorder) 134 | }) 135 | } 136 | } 137 | 138 | func randomAccount(owner string) db.Account { 139 | return db.Account{ 140 | ID: util.RandomInt(1, 1000), 141 | Owner: owner, 142 | Balance: util.RandomMoney(), 143 | Currency: util.RandomCurrency(), 144 | } 145 | } 146 | 147 | func requiredBodyMatchAccount(t *testing.T, body *bytes.Buffer, account db.Account) { 148 | data, err := io.ReadAll(body) 149 | require.NoError(t, err) 150 | 151 | var gotAccount db.Account 152 | err = json.Unmarshal(data, &gotAccount) 153 | require.NoError(t, err) 154 | require.Equal(t, account, gotAccount) 155 | } 156 | -------------------------------------------------------------------------------- /api/account.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | // "go/token" 7 | "log" 8 | "net/http" 9 | 10 | db "github.com/CeciliaChen/simplebank/db/sqlc" 11 | "github.com/CeciliaChen/simplebank/token" 12 | "github.com/gin-gonic/gin" 13 | "github.com/lib/pq" 14 | ) 15 | 16 | type createAccountRequest struct { 17 | Owner string `json:"owner" binding:"required"` 18 | Currency string `json:"currency" binding:"required,currency"` 19 | } 20 | 21 | func (server *Server) createAccount(ctx *gin.Context) { 22 | var req createAccountRequest 23 | if err := ctx.ShouldBindJSON(&req); err != nil { 24 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 25 | return 26 | } 27 | 28 | authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) 29 | arg := db.CreateAccountParams{ 30 | Owner: authPayload.Username, 31 | Currency: req.Currency, 32 | Balance: 0, 33 | } 34 | 35 | account, err := server.store.CreateAccount(ctx, arg) 36 | if err != nil { 37 | if pqErr, ok := err.(*pq.Error); ok { 38 | log.Println(pqErr.Code.Name()) 39 | switch pqErr.Code.Name() { 40 | case "foreign_key_violation", "unique_violation": 41 | ctx.JSON(http.StatusForbidden, errorResponse(err)) 42 | return 43 | } 44 | } 45 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 46 | return 47 | } 48 | 49 | ctx.JSON(http.StatusOK, account) 50 | } 51 | 52 | type getAccountRequest struct { 53 | ID int64 `uri:"id" binding:"required,min=1"` 54 | } 55 | 56 | func (server *Server) getAccount(ctx *gin.Context) { 57 | var req getAccountRequest 58 | if err := ctx.ShouldBindUri(&req); err != nil { 59 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 60 | return 61 | } 62 | 63 | account, err := server.store.GetAccount(ctx, req.ID) 64 | if err != nil { 65 | if err == sql.ErrNoRows { 66 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 67 | return 68 | } 69 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 70 | return 71 | } 72 | 73 | authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) 74 | if account.Owner != authPayload.Username { 75 | err := errors.New("account doesn't belong to the authenticated user") 76 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 77 | return 78 | } 79 | 80 | ctx.JSON(http.StatusOK, account) 81 | } 82 | 83 | type listAccountRequest struct { 84 | PageID int32 `form:"page_id" binding:"required,min=1"` 85 | PageSize int32 `form:"page_size" binding:"required,min=5,max=10"` 86 | } 87 | 88 | func (server *Server) listAccount(ctx *gin.Context) { 89 | var req listAccountRequest 90 | if err := ctx.ShouldBindQuery(&req); err != nil { 91 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 92 | return 93 | } 94 | 95 | authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) 96 | arg := db.ListAccountsParams{ 97 | Owner: authPayload.Username, 98 | Limit: req.PageSize, 99 | Offset: (req.PageID - 1) * req.PageSize, 100 | } 101 | account, err := server.store.ListAccounts(ctx, arg) 102 | if err != nil { 103 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 104 | return 105 | } 106 | 107 | ctx.JSON(http.StatusOK, account) 108 | } 109 | 110 | type deleteAccountRequest struct { 111 | ID int64 `uri:"id" binding:"required,min=1"` 112 | } 113 | 114 | func (server *Server) deleteAccount(ctx *gin.Context) { 115 | var req deleteAccountRequest 116 | if err := ctx.ShouldBindUri(&req); err != nil { 117 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 118 | return 119 | } 120 | 121 | // First, try to fetch the account details 122 | account, err := server.store.GetAccount(ctx, req.ID) 123 | if err != nil { 124 | if err == sql.ErrNoRows { 125 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 126 | return 127 | } 128 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 129 | return 130 | } 131 | 132 | // If fetching is successful, proceed to delete the account 133 | err = server.store.DeleteAccount(ctx, req.ID) 134 | if err != nil { 135 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 136 | return 137 | } 138 | 139 | // Return the fetched account details as the account has been successfully deleted 140 | ctx.JSON(http.StatusOK, account) 141 | } 142 | 143 | type addAccountBalanceRequest struct { 144 | Amount int64 `json:"amount" binding:"required"` 145 | ID int64 `json:"id" binding:"required,min=1"` 146 | } 147 | 148 | func (server *Server) addAccountBalance(ctx *gin.Context) { 149 | var req addAccountBalanceRequest 150 | 151 | if err := ctx.ShouldBindJSON(&req); err != nil { 152 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 153 | return 154 | } 155 | 156 | arg := db.AddAccountBalanceParams{ 157 | Amount: req.Amount, 158 | ID: req.ID, 159 | } 160 | 161 | account, err := server.store.AddAccountBalance(ctx, arg) 162 | if err != nil { 163 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 164 | return 165 | } 166 | 167 | ctx.JSON(http.StatusOK, account) 168 | } 169 | 170 | type updateAccountRequest struct { 171 | ID int64 `json:"id" binding:"required,min=1"` 172 | Balance int64 `json:"balance" binding:"required"` 173 | } 174 | 175 | func (server *Server) updateAccount(ctx *gin.Context) { 176 | var req updateAccountRequest 177 | 178 | // Binding JSON body to struct 179 | if err := ctx.ShouldBindJSON(&req); err != nil { 180 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 181 | return 182 | } 183 | 184 | // Check if the account exists 185 | _, err := server.store.GetAccount(ctx, req.ID) 186 | if err != nil { 187 | if err == sql.ErrNoRows { 188 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 189 | return 190 | } 191 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 192 | return 193 | } 194 | 195 | // Proceed with the update since the account exists 196 | arg := db.UpdateAccountParams{ 197 | ID: req.ID, 198 | Balance: req.Balance, 199 | } 200 | 201 | account, err := server.store.UpdateAccount(ctx, arg) 202 | if err != nil { 203 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 204 | return 205 | } 206 | 207 | ctx.JSON(http.StatusOK, account) 208 | } 209 | -------------------------------------------------------------------------------- /api/user_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/lib/pq" 15 | 16 | mockdb "github.com/CeciliaChen/simplebank/db/mock" 17 | db "github.com/CeciliaChen/simplebank/db/sqlc" 18 | "github.com/CeciliaChen/simplebank/util" 19 | "github.com/gin-gonic/gin" 20 | "github.com/golang/mock/gomock" 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | type eqCreateUserParamsMatcher struct { 25 | arg db.CreateUserParams 26 | password string 27 | } 28 | 29 | func (e eqCreateUserParamsMatcher) Matches(x interface{}) bool { 30 | arg, ok := x.(db.CreateUserParams) 31 | if !ok { 32 | return false 33 | } 34 | 35 | err := util.CheckPassword(e.password, arg.HashedPassword) 36 | if err != nil { 37 | return false 38 | } 39 | 40 | e.arg.HashedPassword = arg.HashedPassword 41 | return reflect.DeepEqual(e.arg, arg) 42 | } 43 | 44 | func (e eqCreateUserParamsMatcher) String() string { 45 | return fmt.Sprintf("matches arg %v and password %v", e.arg, e.password) 46 | } 47 | 48 | func EqCreateUserParams(arg db.CreateUserParams, password string) gomock.Matcher { 49 | return eqCreateUserParamsMatcher{arg, password} 50 | } 51 | 52 | func TestCreateUserAPI(t *testing.T) { 53 | user, password := randomUser(t) 54 | 55 | testCases := []struct { 56 | name string 57 | body gin.H 58 | buildStubs func(store *mockdb.MockStore) 59 | checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder) 60 | }{ 61 | { 62 | name: "OK", 63 | body: gin.H{ 64 | "username": user.Username, 65 | "password": password, 66 | "full_name": user.FullName, 67 | "email": user.Email, 68 | }, 69 | buildStubs: func(store *mockdb.MockStore) { 70 | arg := db.CreateUserParams{ 71 | Username: user.Username, 72 | FullName: user.FullName, 73 | Email: user.Email, 74 | } 75 | store.EXPECT().CreateUser(gomock.Any(), EqCreateUserParams(arg, password)).Times(1).Return(user, nil) 76 | }, 77 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 78 | require.Equal(t, http.StatusOK, recorder.Code) 79 | requireBodyMatchUser(t, recorder.Body, user) 80 | }, 81 | }, 82 | { 83 | name: "InternalError", 84 | body: gin.H{ 85 | "username": user.Username, 86 | "password": password, 87 | "full_name": user.FullName, 88 | "email": user.Email, 89 | }, 90 | buildStubs: func(store *mockdb.MockStore) { 91 | store.EXPECT().CreateUser(gomock.Any(), gomock.Any()).Times(1).Return(db.User{}, sql.ErrConnDone) 92 | }, 93 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 94 | require.Equal(t, http.StatusInternalServerError, recorder.Code) 95 | }, 96 | }, 97 | { 98 | name: "DuplicateUsername", 99 | body: gin.H{ 100 | "username": user.Username, 101 | "password": password, 102 | "full_name": user.FullName, 103 | "email": user.Email, 104 | }, 105 | buildStubs: func(store *mockdb.MockStore) { 106 | store.EXPECT().CreateUser(gomock.Any(), gomock.Any()).Times(1).Return(db.User{}, &pq.Error{Code: "23505"}) 107 | }, 108 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 109 | require.Equal(t, http.StatusForbidden, recorder.Code) 110 | }, 111 | }, 112 | { 113 | name: "InvalidUsername", 114 | body: gin.H{ 115 | "username": "invalid-user#1", 116 | "password": password, 117 | "full_name": user.FullName, 118 | "email": user.Email, 119 | }, 120 | buildStubs: func(store *mockdb.MockStore) { 121 | store.EXPECT().CreateUser(gomock.Any(), gomock.Any()).Times(0) 122 | }, 123 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 124 | require.Equal(t, http.StatusBadRequest, recorder.Code) 125 | }, 126 | }, 127 | { 128 | name: "InvalidEmail", 129 | body: gin.H{ 130 | "username": user.Username, 131 | "password": password, 132 | "full_name": user.FullName, 133 | "email": "invalid-email", 134 | }, 135 | buildStubs: func(store *mockdb.MockStore) { 136 | store.EXPECT().CreateUser(gomock.Any(), gomock.Any()).Times(0) 137 | }, 138 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 139 | require.Equal(t, http.StatusBadRequest, recorder.Code) 140 | }, 141 | }, 142 | { 143 | name: "TooShortPassword", 144 | body: gin.H{ 145 | "username": user.Username, 146 | "password": "123", 147 | "full_name": user.FullName, 148 | "email": user.Email, 149 | }, 150 | buildStubs: func(store *mockdb.MockStore) { 151 | store.EXPECT().CreateUser(gomock.Any(), gomock.Any()).Times(0) 152 | }, 153 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 154 | require.Equal(t, http.StatusBadRequest, recorder.Code) 155 | }, 156 | }, 157 | } 158 | 159 | for i := range testCases { 160 | tc := testCases[i] 161 | 162 | t.Run(tc.name, func(t *testing.T) { 163 | ctrl := gomock.NewController(t) 164 | defer ctrl.Finish() 165 | 166 | store := mockdb.NewMockStore(ctrl) 167 | tc.buildStubs(store) 168 | 169 | server := newTestServer(t, store) 170 | recorder := httptest.NewRecorder() 171 | 172 | // Marshal bodt data to JSON 173 | data, err := json.Marshal(tc.body) 174 | require.NoError(t, err) 175 | 176 | url := "/users" 177 | request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) 178 | require.NoError(t, err) 179 | 180 | server.router.ServeHTTP(recorder, request) 181 | tc.checkResponse(t, recorder) 182 | }) 183 | } 184 | } 185 | 186 | func randomUser(t *testing.T) (user db.User, password string) { 187 | password = util.RandomString(6) 188 | hashedPassword, err := util.HashPassword(password) 189 | require.NoError(t, err) 190 | 191 | user = db.User{ 192 | Username: util.RandomOwner(), 193 | HashedPassword: hashedPassword, 194 | FullName: util.RandomOwner(), 195 | Email: util.RandomEmail(), 196 | } 197 | return 198 | } 199 | 200 | func requireBodyMatchUser(t *testing.T, body *bytes.Buffer, user db.User) { 201 | data, err := io.ReadAll(body) 202 | require.NoError(t, err) 203 | 204 | var gotUser db.User 205 | err = json.Unmarshal(data, &gotUser) 206 | 207 | require.NoError(t, err) 208 | require.Equal(t, user.Username, gotUser.Username) 209 | require.Equal(t, user.FullName, gotUser.FullName) 210 | require.Equal(t, user.Email, gotUser.Email) 211 | require.Empty(t, gotUser.HashedPassword) 212 | } 213 | -------------------------------------------------------------------------------- /db/sqlc/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.22.0 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | "fmt" 11 | ) 12 | 13 | type DBTX interface { 14 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 15 | PrepareContext(context.Context, string) (*sql.Stmt, error) 16 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 17 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 18 | } 19 | 20 | func New(db DBTX) *Queries { 21 | return &Queries{db: db} 22 | } 23 | 24 | func Prepare(ctx context.Context, db DBTX) (*Queries, error) { 25 | q := Queries{db: db} 26 | var err error 27 | if q.addAccountBalanceStmt, err = db.PrepareContext(ctx, addAccountBalance); err != nil { 28 | return nil, fmt.Errorf("error preparing query AddAccountBalance: %w", err) 29 | } 30 | if q.createAccountStmt, err = db.PrepareContext(ctx, createAccount); err != nil { 31 | return nil, fmt.Errorf("error preparing query CreateAccount: %w", err) 32 | } 33 | if q.createEntryStmt, err = db.PrepareContext(ctx, createEntry); err != nil { 34 | return nil, fmt.Errorf("error preparing query CreateEntry: %w", err) 35 | } 36 | if q.createTransferStmt, err = db.PrepareContext(ctx, createTransfer); err != nil { 37 | return nil, fmt.Errorf("error preparing query CreateTransfer: %w", err) 38 | } 39 | if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil { 40 | return nil, fmt.Errorf("error preparing query CreateUser: %w", err) 41 | } 42 | if q.deleteAccountStmt, err = db.PrepareContext(ctx, deleteAccount); err != nil { 43 | return nil, fmt.Errorf("error preparing query DeleteAccount: %w", err) 44 | } 45 | if q.getAccountStmt, err = db.PrepareContext(ctx, getAccount); err != nil { 46 | return nil, fmt.Errorf("error preparing query GetAccount: %w", err) 47 | } 48 | if q.getAccountForUpdateStmt, err = db.PrepareContext(ctx, getAccountForUpdate); err != nil { 49 | return nil, fmt.Errorf("error preparing query GetAccountForUpdate: %w", err) 50 | } 51 | if q.getEntryStmt, err = db.PrepareContext(ctx, getEntry); err != nil { 52 | return nil, fmt.Errorf("error preparing query GetEntry: %w", err) 53 | } 54 | if q.getTransferStmt, err = db.PrepareContext(ctx, getTransfer); err != nil { 55 | return nil, fmt.Errorf("error preparing query GetTransfer: %w", err) 56 | } 57 | if q.getUserStmt, err = db.PrepareContext(ctx, getUser); err != nil { 58 | return nil, fmt.Errorf("error preparing query GetUser: %w", err) 59 | } 60 | if q.listAccountsStmt, err = db.PrepareContext(ctx, listAccounts); err != nil { 61 | return nil, fmt.Errorf("error preparing query ListAccounts: %w", err) 62 | } 63 | if q.listEntriesStmt, err = db.PrepareContext(ctx, listEntries); err != nil { 64 | return nil, fmt.Errorf("error preparing query ListEntries: %w", err) 65 | } 66 | if q.listTransfersStmt, err = db.PrepareContext(ctx, listTransfers); err != nil { 67 | return nil, fmt.Errorf("error preparing query ListTransfers: %w", err) 68 | } 69 | if q.updateAccountStmt, err = db.PrepareContext(ctx, updateAccount); err != nil { 70 | return nil, fmt.Errorf("error preparing query UpdateAccount: %w", err) 71 | } 72 | return &q, nil 73 | } 74 | 75 | func (q *Queries) Close() error { 76 | var err error 77 | if q.addAccountBalanceStmt != nil { 78 | if cerr := q.addAccountBalanceStmt.Close(); cerr != nil { 79 | err = fmt.Errorf("error closing addAccountBalanceStmt: %w", cerr) 80 | } 81 | } 82 | if q.createAccountStmt != nil { 83 | if cerr := q.createAccountStmt.Close(); cerr != nil { 84 | err = fmt.Errorf("error closing createAccountStmt: %w", cerr) 85 | } 86 | } 87 | if q.createEntryStmt != nil { 88 | if cerr := q.createEntryStmt.Close(); cerr != nil { 89 | err = fmt.Errorf("error closing createEntryStmt: %w", cerr) 90 | } 91 | } 92 | if q.createTransferStmt != nil { 93 | if cerr := q.createTransferStmt.Close(); cerr != nil { 94 | err = fmt.Errorf("error closing createTransferStmt: %w", cerr) 95 | } 96 | } 97 | if q.createUserStmt != nil { 98 | if cerr := q.createUserStmt.Close(); cerr != nil { 99 | err = fmt.Errorf("error closing createUserStmt: %w", cerr) 100 | } 101 | } 102 | if q.deleteAccountStmt != nil { 103 | if cerr := q.deleteAccountStmt.Close(); cerr != nil { 104 | err = fmt.Errorf("error closing deleteAccountStmt: %w", cerr) 105 | } 106 | } 107 | if q.getAccountStmt != nil { 108 | if cerr := q.getAccountStmt.Close(); cerr != nil { 109 | err = fmt.Errorf("error closing getAccountStmt: %w", cerr) 110 | } 111 | } 112 | if q.getAccountForUpdateStmt != nil { 113 | if cerr := q.getAccountForUpdateStmt.Close(); cerr != nil { 114 | err = fmt.Errorf("error closing getAccountForUpdateStmt: %w", cerr) 115 | } 116 | } 117 | if q.getEntryStmt != nil { 118 | if cerr := q.getEntryStmt.Close(); cerr != nil { 119 | err = fmt.Errorf("error closing getEntryStmt: %w", cerr) 120 | } 121 | } 122 | if q.getTransferStmt != nil { 123 | if cerr := q.getTransferStmt.Close(); cerr != nil { 124 | err = fmt.Errorf("error closing getTransferStmt: %w", cerr) 125 | } 126 | } 127 | if q.getUserStmt != nil { 128 | if cerr := q.getUserStmt.Close(); cerr != nil { 129 | err = fmt.Errorf("error closing getUserStmt: %w", cerr) 130 | } 131 | } 132 | if q.listAccountsStmt != nil { 133 | if cerr := q.listAccountsStmt.Close(); cerr != nil { 134 | err = fmt.Errorf("error closing listAccountsStmt: %w", cerr) 135 | } 136 | } 137 | if q.listEntriesStmt != nil { 138 | if cerr := q.listEntriesStmt.Close(); cerr != nil { 139 | err = fmt.Errorf("error closing listEntriesStmt: %w", cerr) 140 | } 141 | } 142 | if q.listTransfersStmt != nil { 143 | if cerr := q.listTransfersStmt.Close(); cerr != nil { 144 | err = fmt.Errorf("error closing listTransfersStmt: %w", cerr) 145 | } 146 | } 147 | if q.updateAccountStmt != nil { 148 | if cerr := q.updateAccountStmt.Close(); cerr != nil { 149 | err = fmt.Errorf("error closing updateAccountStmt: %w", cerr) 150 | } 151 | } 152 | return err 153 | } 154 | 155 | func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { 156 | switch { 157 | case stmt != nil && q.tx != nil: 158 | return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) 159 | case stmt != nil: 160 | return stmt.ExecContext(ctx, args...) 161 | default: 162 | return q.db.ExecContext(ctx, query, args...) 163 | } 164 | } 165 | 166 | func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { 167 | switch { 168 | case stmt != nil && q.tx != nil: 169 | return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) 170 | case stmt != nil: 171 | return stmt.QueryContext(ctx, args...) 172 | default: 173 | return q.db.QueryContext(ctx, query, args...) 174 | } 175 | } 176 | 177 | func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row { 178 | switch { 179 | case stmt != nil && q.tx != nil: 180 | return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) 181 | case stmt != nil: 182 | return stmt.QueryRowContext(ctx, args...) 183 | default: 184 | return q.db.QueryRowContext(ctx, query, args...) 185 | } 186 | } 187 | 188 | type Queries struct { 189 | db DBTX 190 | tx *sql.Tx 191 | addAccountBalanceStmt *sql.Stmt 192 | createAccountStmt *sql.Stmt 193 | createEntryStmt *sql.Stmt 194 | createTransferStmt *sql.Stmt 195 | createUserStmt *sql.Stmt 196 | deleteAccountStmt *sql.Stmt 197 | getAccountStmt *sql.Stmt 198 | getAccountForUpdateStmt *sql.Stmt 199 | getEntryStmt *sql.Stmt 200 | getTransferStmt *sql.Stmt 201 | getUserStmt *sql.Stmt 202 | listAccountsStmt *sql.Stmt 203 | listEntriesStmt *sql.Stmt 204 | listTransfersStmt *sql.Stmt 205 | updateAccountStmt *sql.Stmt 206 | } 207 | 208 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 209 | return &Queries{ 210 | db: tx, 211 | tx: tx, 212 | addAccountBalanceStmt: q.addAccountBalanceStmt, 213 | createAccountStmt: q.createAccountStmt, 214 | createEntryStmt: q.createEntryStmt, 215 | createTransferStmt: q.createTransferStmt, 216 | createUserStmt: q.createUserStmt, 217 | deleteAccountStmt: q.deleteAccountStmt, 218 | getAccountStmt: q.getAccountStmt, 219 | getAccountForUpdateStmt: q.getAccountForUpdateStmt, 220 | getEntryStmt: q.getEntryStmt, 221 | getTransferStmt: q.getTransferStmt, 222 | getUserStmt: q.getUserStmt, 223 | listAccountsStmt: q.listAccountsStmt, 224 | listEntriesStmt: q.listEntriesStmt, 225 | listTransfersStmt: q.listTransfersStmt, 226 | updateAccountStmt: q.updateAccountStmt, 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /db/mock/store.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/CeciliaChen/simplebank/db/sqlc (interfaces: Store) 3 | 4 | // Package mockdb is a generated GoMock package. 5 | package mockdb 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | db "github.com/CeciliaChen/simplebank/db/sqlc" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockStore is a mock of Store interface. 16 | type MockStore struct { 17 | ctrl *gomock.Controller 18 | recorder *MockStoreMockRecorder 19 | } 20 | 21 | // MockStoreMockRecorder is the mock recorder for MockStore. 22 | type MockStoreMockRecorder struct { 23 | mock *MockStore 24 | } 25 | 26 | // NewMockStore creates a new mock instance. 27 | func NewMockStore(ctrl *gomock.Controller) *MockStore { 28 | mock := &MockStore{ctrl: ctrl} 29 | mock.recorder = &MockStoreMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockStore) EXPECT() *MockStoreMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // AddAccountBalance mocks base method. 39 | func (m *MockStore) AddAccountBalance(arg0 context.Context, arg1 db.AddAccountBalanceParams) (db.Account, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "AddAccountBalance", arg0, arg1) 42 | ret0, _ := ret[0].(db.Account) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // AddAccountBalance indicates an expected call of AddAccountBalance. 48 | func (mr *MockStoreMockRecorder) AddAccountBalance(arg0, arg1 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAccountBalance", reflect.TypeOf((*MockStore)(nil).AddAccountBalance), arg0, arg1) 51 | } 52 | 53 | // CreateAccount mocks base method. 54 | func (m *MockStore) CreateAccount(arg0 context.Context, arg1 db.CreateAccountParams) (db.Account, error) { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "CreateAccount", arg0, arg1) 57 | ret0, _ := ret[0].(db.Account) 58 | ret1, _ := ret[1].(error) 59 | return ret0, ret1 60 | } 61 | 62 | // CreateAccount indicates an expected call of CreateAccount. 63 | func (mr *MockStoreMockRecorder) CreateAccount(arg0, arg1 interface{}) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccount", reflect.TypeOf((*MockStore)(nil).CreateAccount), arg0, arg1) 66 | } 67 | 68 | // CreateEntry mocks base method. 69 | func (m *MockStore) CreateEntry(arg0 context.Context, arg1 db.CreateEntryParams) (db.Entry, error) { 70 | m.ctrl.T.Helper() 71 | ret := m.ctrl.Call(m, "CreateEntry", arg0, arg1) 72 | ret0, _ := ret[0].(db.Entry) 73 | ret1, _ := ret[1].(error) 74 | return ret0, ret1 75 | } 76 | 77 | // CreateEntry indicates an expected call of CreateEntry. 78 | func (mr *MockStoreMockRecorder) CreateEntry(arg0, arg1 interface{}) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEntry", reflect.TypeOf((*MockStore)(nil).CreateEntry), arg0, arg1) 81 | } 82 | 83 | // CreateTransfer mocks base method. 84 | func (m *MockStore) CreateTransfer(arg0 context.Context, arg1 db.CreateTransferParams) (db.Transfer, error) { 85 | m.ctrl.T.Helper() 86 | ret := m.ctrl.Call(m, "CreateTransfer", arg0, arg1) 87 | ret0, _ := ret[0].(db.Transfer) 88 | ret1, _ := ret[1].(error) 89 | return ret0, ret1 90 | } 91 | 92 | // CreateTransfer indicates an expected call of CreateTransfer. 93 | func (mr *MockStoreMockRecorder) CreateTransfer(arg0, arg1 interface{}) *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransfer", reflect.TypeOf((*MockStore)(nil).CreateTransfer), arg0, arg1) 96 | } 97 | 98 | // CreateUser mocks base method. 99 | func (m *MockStore) CreateUser(arg0 context.Context, arg1 db.CreateUserParams) (db.User, error) { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "CreateUser", arg0, arg1) 102 | ret0, _ := ret[0].(db.User) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // CreateUser indicates an expected call of CreateUser. 108 | func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0, arg1) 111 | } 112 | 113 | // DeleteAccount mocks base method. 114 | func (m *MockStore) DeleteAccount(arg0 context.Context, arg1 int64) error { 115 | m.ctrl.T.Helper() 116 | ret := m.ctrl.Call(m, "DeleteAccount", arg0, arg1) 117 | ret0, _ := ret[0].(error) 118 | return ret0 119 | } 120 | 121 | // DeleteAccount indicates an expected call of DeleteAccount. 122 | func (mr *MockStoreMockRecorder) DeleteAccount(arg0, arg1 interface{}) *gomock.Call { 123 | mr.mock.ctrl.T.Helper() 124 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccount", reflect.TypeOf((*MockStore)(nil).DeleteAccount), arg0, arg1) 125 | } 126 | 127 | // GetAccount mocks base method. 128 | func (m *MockStore) GetAccount(arg0 context.Context, arg1 int64) (db.Account, error) { 129 | m.ctrl.T.Helper() 130 | ret := m.ctrl.Call(m, "GetAccount", arg0, arg1) 131 | ret0, _ := ret[0].(db.Account) 132 | ret1, _ := ret[1].(error) 133 | return ret0, ret1 134 | } 135 | 136 | // GetAccount indicates an expected call of GetAccount. 137 | func (mr *MockStoreMockRecorder) GetAccount(arg0, arg1 interface{}) *gomock.Call { 138 | mr.mock.ctrl.T.Helper() 139 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockStore)(nil).GetAccount), arg0, arg1) 140 | } 141 | 142 | // GetAccountForUpdate mocks base method. 143 | func (m *MockStore) GetAccountForUpdate(arg0 context.Context, arg1 int64) (db.Account, error) { 144 | m.ctrl.T.Helper() 145 | ret := m.ctrl.Call(m, "GetAccountForUpdate", arg0, arg1) 146 | ret0, _ := ret[0].(db.Account) 147 | ret1, _ := ret[1].(error) 148 | return ret0, ret1 149 | } 150 | 151 | // GetAccountForUpdate indicates an expected call of GetAccountForUpdate. 152 | func (mr *MockStoreMockRecorder) GetAccountForUpdate(arg0, arg1 interface{}) *gomock.Call { 153 | mr.mock.ctrl.T.Helper() 154 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountForUpdate", reflect.TypeOf((*MockStore)(nil).GetAccountForUpdate), arg0, arg1) 155 | } 156 | 157 | // GetEntry mocks base method. 158 | func (m *MockStore) GetEntry(arg0 context.Context, arg1 int64) (db.Entry, error) { 159 | m.ctrl.T.Helper() 160 | ret := m.ctrl.Call(m, "GetEntry", arg0, arg1) 161 | ret0, _ := ret[0].(db.Entry) 162 | ret1, _ := ret[1].(error) 163 | return ret0, ret1 164 | } 165 | 166 | // GetEntry indicates an expected call of GetEntry. 167 | func (mr *MockStoreMockRecorder) GetEntry(arg0, arg1 interface{}) *gomock.Call { 168 | mr.mock.ctrl.T.Helper() 169 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntry", reflect.TypeOf((*MockStore)(nil).GetEntry), arg0, arg1) 170 | } 171 | 172 | // GetTransfer mocks base method. 173 | func (m *MockStore) GetTransfer(arg0 context.Context, arg1 int64) (db.Transfer, error) { 174 | m.ctrl.T.Helper() 175 | ret := m.ctrl.Call(m, "GetTransfer", arg0, arg1) 176 | ret0, _ := ret[0].(db.Transfer) 177 | ret1, _ := ret[1].(error) 178 | return ret0, ret1 179 | } 180 | 181 | // GetTransfer indicates an expected call of GetTransfer. 182 | func (mr *MockStoreMockRecorder) GetTransfer(arg0, arg1 interface{}) *gomock.Call { 183 | mr.mock.ctrl.T.Helper() 184 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransfer", reflect.TypeOf((*MockStore)(nil).GetTransfer), arg0, arg1) 185 | } 186 | 187 | // GetUser mocks base method. 188 | func (m *MockStore) GetUser(arg0 context.Context, arg1 string) (db.User, error) { 189 | m.ctrl.T.Helper() 190 | ret := m.ctrl.Call(m, "GetUser", arg0, arg1) 191 | ret0, _ := ret[0].(db.User) 192 | ret1, _ := ret[1].(error) 193 | return ret0, ret1 194 | } 195 | 196 | // GetUser indicates an expected call of GetUser. 197 | func (mr *MockStoreMockRecorder) GetUser(arg0, arg1 interface{}) *gomock.Call { 198 | mr.mock.ctrl.T.Helper() 199 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockStore)(nil).GetUser), arg0, arg1) 200 | } 201 | 202 | // ListAccounts mocks base method. 203 | func (m *MockStore) ListAccounts(arg0 context.Context, arg1 db.ListAccountsParams) ([]db.Account, error) { 204 | m.ctrl.T.Helper() 205 | ret := m.ctrl.Call(m, "ListAccounts", arg0, arg1) 206 | ret0, _ := ret[0].([]db.Account) 207 | ret1, _ := ret[1].(error) 208 | return ret0, ret1 209 | } 210 | 211 | // ListAccounts indicates an expected call of ListAccounts. 212 | func (mr *MockStoreMockRecorder) ListAccounts(arg0, arg1 interface{}) *gomock.Call { 213 | mr.mock.ctrl.T.Helper() 214 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccounts", reflect.TypeOf((*MockStore)(nil).ListAccounts), arg0, arg1) 215 | } 216 | 217 | // ListEntries mocks base method. 218 | func (m *MockStore) ListEntries(arg0 context.Context, arg1 db.ListEntriesParams) ([]db.Entry, error) { 219 | m.ctrl.T.Helper() 220 | ret := m.ctrl.Call(m, "ListEntries", arg0, arg1) 221 | ret0, _ := ret[0].([]db.Entry) 222 | ret1, _ := ret[1].(error) 223 | return ret0, ret1 224 | } 225 | 226 | // ListEntries indicates an expected call of ListEntries. 227 | func (mr *MockStoreMockRecorder) ListEntries(arg0, arg1 interface{}) *gomock.Call { 228 | mr.mock.ctrl.T.Helper() 229 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEntries", reflect.TypeOf((*MockStore)(nil).ListEntries), arg0, arg1) 230 | } 231 | 232 | // ListTransfers mocks base method. 233 | func (m *MockStore) ListTransfers(arg0 context.Context, arg1 db.ListTransfersParams) ([]db.Transfer, error) { 234 | m.ctrl.T.Helper() 235 | ret := m.ctrl.Call(m, "ListTransfers", arg0, arg1) 236 | ret0, _ := ret[0].([]db.Transfer) 237 | ret1, _ := ret[1].(error) 238 | return ret0, ret1 239 | } 240 | 241 | // ListTransfers indicates an expected call of ListTransfers. 242 | func (mr *MockStoreMockRecorder) ListTransfers(arg0, arg1 interface{}) *gomock.Call { 243 | mr.mock.ctrl.T.Helper() 244 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTransfers", reflect.TypeOf((*MockStore)(nil).ListTransfers), arg0, arg1) 245 | } 246 | 247 | // TransferTx mocks base method. 248 | func (m *MockStore) TransferTx(arg0 context.Context, arg1 db.TransferTxParams) (db.TransferTxResult, error) { 249 | m.ctrl.T.Helper() 250 | ret := m.ctrl.Call(m, "TransferTx", arg0, arg1) 251 | ret0, _ := ret[0].(db.TransferTxResult) 252 | ret1, _ := ret[1].(error) 253 | return ret0, ret1 254 | } 255 | 256 | // TransferTx indicates an expected call of TransferTx. 257 | func (mr *MockStoreMockRecorder) TransferTx(arg0, arg1 interface{}) *gomock.Call { 258 | mr.mock.ctrl.T.Helper() 259 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransferTx", reflect.TypeOf((*MockStore)(nil).TransferTx), arg0, arg1) 260 | } 261 | 262 | // UpdateAccount mocks base method. 263 | func (m *MockStore) UpdateAccount(arg0 context.Context, arg1 db.UpdateAccountParams) (db.Account, error) { 264 | m.ctrl.T.Helper() 265 | ret := m.ctrl.Call(m, "UpdateAccount", arg0, arg1) 266 | ret0, _ := ret[0].(db.Account) 267 | ret1, _ := ret[1].(error) 268 | return ret0, ret1 269 | } 270 | 271 | // UpdateAccount indicates an expected call of UpdateAccount. 272 | func (mr *MockStoreMockRecorder) UpdateAccount(arg0, arg1 interface{}) *gomock.Call { 273 | mr.mock.ctrl.T.Helper() 274 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccount", reflect.TypeOf((*MockStore)(nil).UpdateAccount), arg0, arg1) 275 | } 276 | -------------------------------------------------------------------------------- /api/transfer_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "encoding/json" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | 12 | mockdb "github.com/CeciliaChen/simplebank/db/mock" 13 | db "github.com/CeciliaChen/simplebank/db/sqlc" 14 | "github.com/CeciliaChen/simplebank/token" 15 | "github.com/CeciliaChen/simplebank/util" 16 | "github.com/gin-gonic/gin" 17 | "github.com/golang/mock/gomock" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestTransferAPI(t *testing.T) { 22 | amount := int64(10) 23 | 24 | user1, _ := randomUser(t) 25 | user2, _ := randomUser(t) 26 | user3, _ := randomUser(t) 27 | 28 | account1 := randomAccount(user1.Username) 29 | account2 := randomAccount(user2.Username) 30 | account3 := randomAccount(user3.Username) 31 | 32 | account1.Currency = util.USD 33 | account2.Currency = util.USD 34 | account3.Currency = util.EUR 35 | 36 | testCases := []struct { 37 | name string 38 | body gin.H 39 | setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker) 40 | buildStubs func(store *mockdb.MockStore) 41 | checkResponse func(recoder *httptest.ResponseRecorder) 42 | }{ 43 | { 44 | name: "OK", 45 | body: gin.H{ 46 | "from_account_id": account1.ID, 47 | "to_account_id": account2.ID, 48 | "amount": amount, 49 | "currency": util.USD, 50 | }, 51 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 52 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) 53 | }, 54 | buildStubs: func(store *mockdb.MockStore) { 55 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) 56 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account2.ID)).Times(1).Return(account2, nil) 57 | 58 | arg := db.TransferTxParams{ 59 | FromAccountID: account1.ID, 60 | ToAccountID: account2.ID, 61 | Amount: amount, 62 | } 63 | store.EXPECT().TransferTx(gomock.Any(), gomock.Eq(arg)).Times(1) 64 | }, 65 | checkResponse: func(recorder *httptest.ResponseRecorder) { 66 | require.Equal(t, http.StatusOK, recorder.Code) 67 | }, 68 | }, 69 | { 70 | name: "UnauthorizedUser", 71 | body: gin.H{ 72 | "from_account_id": account1.ID, 73 | "to_account_id": account2.ID, 74 | "amount": amount, 75 | "currency": util.USD, 76 | }, 77 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 78 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user2.Username, time.Minute) 79 | }, 80 | buildStubs: func(store *mockdb.MockStore) { 81 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) 82 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account2.ID)).Times(0) 83 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0) 84 | }, 85 | checkResponse: func(recorder *httptest.ResponseRecorder) { 86 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 87 | }, 88 | }, 89 | { 90 | name: "NoAuthorization", 91 | body: gin.H{ 92 | "from_account_id": account1.ID, 93 | "to_account_id": account2.ID, 94 | "amount": amount, 95 | "currency": util.USD, 96 | }, 97 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 98 | }, 99 | buildStubs: func(store *mockdb.MockStore) { 100 | store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) 101 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0) 102 | }, 103 | checkResponse: func(recorder *httptest.ResponseRecorder) { 104 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 105 | }, 106 | }, 107 | { 108 | name: "FromAccountNotFound", 109 | body: gin.H{ 110 | "from_account_id": account1.ID, 111 | "to_account_id": account2.ID, 112 | "amount": amount, 113 | "currency": util.USD, 114 | }, 115 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 116 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) 117 | }, 118 | buildStubs: func(store *mockdb.MockStore) { 119 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(db.Account{}, sql.ErrNoRows) 120 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account2.ID)).Times(0) 121 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0) 122 | }, 123 | checkResponse: func(recorder *httptest.ResponseRecorder) { 124 | require.Equal(t, http.StatusNotFound, recorder.Code) 125 | }, 126 | }, 127 | { 128 | name: "ToAccountNotFound", 129 | body: gin.H{ 130 | "from_account_id": account1.ID, 131 | "to_account_id": account2.ID, 132 | "amount": amount, 133 | "currency": util.USD, 134 | }, 135 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 136 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) 137 | }, 138 | buildStubs: func(store *mockdb.MockStore) { 139 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) 140 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account2.ID)).Times(1).Return(db.Account{}, sql.ErrNoRows) 141 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0) 142 | }, 143 | checkResponse: func(recorder *httptest.ResponseRecorder) { 144 | require.Equal(t, http.StatusNotFound, recorder.Code) 145 | }, 146 | }, 147 | { 148 | name: "FromAccountCurrencyMismatch", 149 | body: gin.H{ 150 | "from_account_id": account3.ID, 151 | "to_account_id": account2.ID, 152 | "amount": amount, 153 | "currency": util.USD, 154 | }, 155 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 156 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user3.Username, time.Minute) 157 | }, 158 | buildStubs: func(store *mockdb.MockStore) { 159 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account3.ID)).Times(1).Return(account3, nil) 160 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account2.ID)).Times(0) 161 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0) 162 | }, 163 | checkResponse: func(recorder *httptest.ResponseRecorder) { 164 | require.Equal(t, http.StatusBadRequest, recorder.Code) 165 | }, 166 | }, 167 | { 168 | name: "ToAccountCurrencyMismatch", 169 | body: gin.H{ 170 | "from_account_id": account1.ID, 171 | "to_account_id": account3.ID, 172 | "amount": amount, 173 | "currency": util.USD, 174 | }, 175 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 176 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) 177 | }, 178 | buildStubs: func(store *mockdb.MockStore) { 179 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) 180 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account3.ID)).Times(1).Return(account3, nil) 181 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0) 182 | }, 183 | checkResponse: func(recorder *httptest.ResponseRecorder) { 184 | require.Equal(t, http.StatusBadRequest, recorder.Code) 185 | }, 186 | }, 187 | { 188 | name: "InvalidCurrency", 189 | body: gin.H{ 190 | "from_account_id": account1.ID, 191 | "to_account_id": account2.ID, 192 | "amount": amount, 193 | "currency": "XYZ", 194 | }, 195 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 196 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) 197 | }, 198 | buildStubs: func(store *mockdb.MockStore) { 199 | store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) 200 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0) 201 | }, 202 | checkResponse: func(recorder *httptest.ResponseRecorder) { 203 | require.Equal(t, http.StatusBadRequest, recorder.Code) 204 | }, 205 | }, 206 | { 207 | name: "NegativeAmount", 208 | body: gin.H{ 209 | "from_account_id": account1.ID, 210 | "to_account_id": account2.ID, 211 | "amount": -amount, 212 | "currency": util.USD, 213 | }, 214 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 215 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) 216 | }, 217 | buildStubs: func(store *mockdb.MockStore) { 218 | store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) 219 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0) 220 | }, 221 | checkResponse: func(recorder *httptest.ResponseRecorder) { 222 | require.Equal(t, http.StatusBadRequest, recorder.Code) 223 | }, 224 | }, 225 | { 226 | name: "GetAccountError", 227 | body: gin.H{ 228 | "from_account_id": account1.ID, 229 | "to_account_id": account2.ID, 230 | "amount": amount, 231 | "currency": util.USD, 232 | }, 233 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 234 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) 235 | }, 236 | buildStubs: func(store *mockdb.MockStore) { 237 | store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(1).Return(db.Account{}, sql.ErrConnDone) 238 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0) 239 | }, 240 | checkResponse: func(recorder *httptest.ResponseRecorder) { 241 | require.Equal(t, http.StatusInternalServerError, recorder.Code) 242 | }, 243 | }, 244 | { 245 | name: "TransferTxError", 246 | body: gin.H{ 247 | "from_account_id": account1.ID, 248 | "to_account_id": account2.ID, 249 | "amount": amount, 250 | "currency": util.USD, 251 | }, 252 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 253 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) 254 | }, 255 | buildStubs: func(store *mockdb.MockStore) { 256 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) 257 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account2.ID)).Times(1).Return(account2, nil) 258 | store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(1).Return(db.TransferTxResult{}, sql.ErrTxDone) 259 | }, 260 | checkResponse: func(recorder *httptest.ResponseRecorder) { 261 | require.Equal(t, http.StatusInternalServerError, recorder.Code) 262 | }, 263 | }, 264 | } 265 | 266 | for i := range testCases { 267 | tc := testCases[i] 268 | 269 | t.Run(tc.name, func(t *testing.T) { 270 | ctrl := gomock.NewController(t) 271 | defer ctrl.Finish() 272 | 273 | store := mockdb.NewMockStore(ctrl) 274 | tc.buildStubs(store) 275 | 276 | server := newTestServer(t, store) 277 | recorder := httptest.NewRecorder() 278 | 279 | // Marshal body data to JSON 280 | data, err := json.Marshal(tc.body) 281 | require.NoError(t, err) 282 | 283 | url := "/transfers" 284 | request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) 285 | require.NoError(t, err) 286 | 287 | tc.setupAuth(t, request, server.tokenMaker) 288 | server.router.ServeHTTP(recorder, request) 289 | tc.checkResponse(recorder) 290 | }) 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to Simple Bank!

2 | 3 | > 😋 This repository hosts my Backend Project for A Simple Bank, a Golang-based web service to create and manage money transfers between available accounts. 😋 4 | 5 | ## 💻 About 6 | Through this project, I've developed a robust backend system for a banking application. 7 | 8 | Key features include account management, transaction records, and inter-account money transfers. 9 | 10 | The project spans several critical areas of backend development, including database design with DBML, transaction handling, API development using the Gin framework, user authentication with JWT and PASETO, and implementing robust testing strategies. Additionally, the repository demonstrates proficiency in deploying applications using Docker and Kubernetes on AWS, complete with domain registration and traffic routing. 11 | 12 | In this project, I've delved deeply into backend web service development, showcasing my practical abilities and grasp of key tools and technologies. I want to use this project as a platform to demonstrate my dedication and eager to learn backend development and my ability to contribute meaningfully in a professional context. 13 | 14 | ## ✅ Key Technologies and Concepts 15 | 16 | This project incorporates a diverse set of technologies and concepts essential for backend development: 17 | 18 | - **Programming Language**: Golang 19 | - **Web Framework**: Gin 20 | - **API Documentation**: Swagger 21 | - **Database Design and Interaction**: DBML, SQL, Database Isolation Levels 22 | - **Security and Authentication**: JWT, PASETO, HTTPS, TLS, Let's Encrypt 23 | - **Containerization and Orchestration**: Docker, Kubernetes 24 | - **Cloud Computing**: AWS, EKS 25 | - **Continuous Integration/Deployment**: GitHub Actions 26 | 27 | ## 🔍 Details 28 | ### 1. Working with database (Postgres + SQLC) 29 |
30 | I designed DB schema and generated SQL code with dbdiagram.io 31 | Simple Bank Schema 32 |
33 | 34 |
35 | Docker + Postgres + TablePlus 36 |

      37 | I used Docker to run containers and chose Postgres 12 as my relational database. For easier look up to the actual data in the local database, I used the TablePlus GUI. When using Docker, I use terminal command extentively, such as docker ps -a, docker start, and docker exec. 38 |

39 |
40 | 41 |
42 | Database Migration 43 |

      44 | When working with a database, schema migration is often necessary to adapt to new business requirements. I ran and managed these database schema migrations using the Golang Migrate library. This library offers various customized commands for migrating schemas up and down. The SQL code for schema migration is stored in the 'db/migration' folder. 45 |

46 |
47 | 48 |
49 | Makefile and .Phony list 50 |

      51 | Remembering and entering various lengthy terminal commands can be exhausting, so I defined those commonly used commands in my Makefile and listed them under .PHONY targets for easy execution. 52 |

53 |
54 | 55 |
56 | Generate CRUD Golang code from SQL 57 |

      58 | After comparing various libraries for converting SQL queries into type-safe Go code, such as database/sql, Gorm, sqlx, and sqlc, I decided to use sqlc for interacting (Create, Read, Update, and Delete operations) with my database. After executing sqlc, it automatically generates struct definitions for models, function definitions with parameters, and the dbtx interface(this allows me to use either a database or a transaction to execute a query). 59 |

60 |
61 | 62 |
63 | Unit Tests for database CRUD & Github Actions CI/CD 64 |

      65 | Using Go's testing package and Testify library(require package), I wrote various unit tests for the database CRUD operations. The files are named ending with "_test.go".
66 |       67 | I used Github Action for running automated tests, I defined test and deployment workflow in .yaml files. 68 | image 69 |

70 |
71 | 72 |
73 | DB Transaction lock, and handle Deadlock 74 |

      75 | In order to show deadlock, I used TDD (test driven development) to create multiple go routine to execute transfer transactions concurrrently, then iterate through the list of results to check the created transfer and entry objects, and finally check the balances of those accounts accordingly.
76 |       77 | A deadlock occurs when multiple processes try to access or modify the same data, with each of them waiting for the others to release the data while continuing to hold onto it themselves. This leads to a situation where none of the processes can actually control the data. A simple solution to prevent deadlocks is to include the 'FOR NO KEY UPDATE' clause in the SQL code. This informs PostgreSQL that the current operation will not modify the foreign key (account id), even though the main purpose of the SQL statement is to change the balance. As a result, the transaction lock does not hold onto the accounts table (where the account id is the primary key), thus reducing the risk of deadlocks. Deadlocks can also occur due to the order in which transactions update shared resources. For example, if Transaction T1 locks Account A first and then tries to lock Account B, while simultaneously Transaction T2 locks Account B and then tries to lock Account A, a deadlock ensues. To resolve this, I implemented a solution that enforces a consistent ordering: always locking the account with the smaller ID before locking the account with a larger ID. 78 |

79 |
80 | 81 |
82 | ACID Property 83 |

      84 | The default isolation level for my PostgreSQL database is 'Repeatable Read'. 85 |

86 |
87 | 88 |
89 | ### 2. Building RESTFul HTTP JSON API [Gin + JWT + PASETO] 90 | 91 |
92 | RESTFul HTTP API 93 |

      94 | I implemented HTTP API in Go using Gin web framework, there are many popular frameworks out there such as Beego, Echo, Revel, Martini, FastHttp, Gorilla Mux, but I chose Gin to do it. Furthermore, I used Postman to test the requests. 95 |

96 | image 97 | 98 |
99 | 100 |
101 | Load Config from files & environment variables 102 |

      103 | I used Viper to load Config from files & environment variables. 104 |

105 |
106 | 107 |
108 | Security 109 |

      110 | I used Bcrypt package to hash user passwords, and upgraded JWT into PASETO token for authentication for login user API. Also, I added bearer-token based authentication layer to the API using Gin so that only the account owner can see the returned list of accounts owned by the corresponding user. 111 |

112 |
113 | 114 |
115 | 116 | ### 3. Deploying the app to production [Docker + Kubernetes + AWS] 117 | I learned 118 | 1. How to build a small Golang docker image with a multistage dockerfile. 119 | 2. How to use docker network to connect 2 stand-alone containers. 120 | 3. How to write docker-compose file and control service start-up orders. 121 | 4. Auto build & push docker image to AWS ECR(Amazon Elastic Container Registry) with Github Actions. 122 | 5. How to create a production database on AWS RDS(Amazon Relational Database Service). 123 | 6. Store & retrieve production secrets with AWS secrets manager. 124 | 7. Kubernetes architexture & How to create an EKS cluster on AWS. 125 | 8. How to use kubectl to connect to a kubernetes cluster on AWS EKS(Elastic Kubernetes Service). 126 | 9. How to deploy a web app to Kubernetes cluster on AWS EKS. 127 | 10. Register a domain & set up A-record using AWS Route53(DNS Service). 128 | 11. How to use Ingress to route traffics to different services in Kubernetes. 129 | 12. Auto issue & renew TSL certificates with cert-manager and Let's Encrypt. 130 | 13. Automatic deploy to Kubernetes with Github Action. 131 | image 132 | image 133 | 134 | 135 | ## 😄 Other Project 136 | ✨ [See my iOS Work Portfolio](https://github.com/ceciliachenguo/iOSAppPortfolio_Cecilia_in_Marlo)
137 | ✨ [See my Task Management mini iOS App with CoreData](https://github.com/ceciliachenguo/TaskManagerCoreData)
138 | ✨ [See my Add, Delete, & Draggable Capsule Tags iOS Project](https://github.com/ceciliachenguo/CapsuleTags)
139 | ✨ [See my iOS Boomerang Cards 3D Animation Project](https://github.com/ceciliachenguo/BoomerangCards)
140 | ✨ [See my iOS Realtime Messaging App using Stream SKD](https://github.com/ceciliachenguo/iMessageClone)
141 | ✨ [See my iOS Book/Reading App Page Flipping 3D Animation Project](https://github.com/ceciliachenguo/BookPageFlipAnimation)
142 | 143 | There is more to explore in my public repositories, and there are many interesting projects that I didn't set to public such as OpenAI API Chatbots and etc. 144 | 145 | ## 👧 Author 146 | Cecilia Chen 147 | 148 | ## 📝 Note 149 | I learned this Golang Back-end Course from TechSchool's Quang Pham and organized this repository myself. I love his teaching style and highly recommend his courses to anyone who needs it. I took notes in Notion and learned a great deal from his backend series course, really appreciate it! 150 | 151 | ## 📮 Contact Me, Thanks!!! 152 | * LinkedIn: https://www.linkedin.com/in/ceciliaguochen/ 153 | * Github: [@ceciliachenguo](https://github.com/ceciliachenguo) 154 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= 42 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 43 | github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb h1:6Z/wqhPFZ7y5ksCEV/V5MXOazLaeu/EW97CU5rz8NWk= 44 | github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= 45 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= 46 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= 47 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 48 | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= 49 | github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= 50 | github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= 51 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 52 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 53 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 54 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= 55 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= 56 | github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= 57 | github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 58 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 59 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 60 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 61 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 62 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 63 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 64 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 65 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 66 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 67 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 68 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 69 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 70 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 71 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 72 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 73 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 74 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 75 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 76 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 77 | github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 78 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 79 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 80 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 81 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 82 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 83 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 84 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 85 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 86 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 87 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 88 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 89 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 90 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 91 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 92 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 93 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 94 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 95 | github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs= 96 | github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 97 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 98 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 99 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 100 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 101 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 102 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 103 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 104 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 105 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 106 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 107 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 108 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 109 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 110 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 111 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 112 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 113 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 114 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 115 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 116 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 117 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 118 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 119 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 120 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 121 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 122 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 123 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 124 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 125 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 126 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 127 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 128 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 129 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 130 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 131 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 132 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 133 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 134 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 135 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 136 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 137 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 138 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 139 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 140 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 141 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 142 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 143 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 144 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 145 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 146 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 147 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 148 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 149 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 150 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 151 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 152 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 153 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 154 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 155 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 156 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 157 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 158 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 159 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 160 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 161 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 162 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 163 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 164 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 165 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 166 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 167 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 168 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 169 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 170 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 171 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 172 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 173 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 174 | github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= 175 | github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 176 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 177 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 178 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 179 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 180 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 181 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 182 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 183 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 184 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 185 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 186 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 187 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 188 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 189 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 190 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 191 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 192 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 193 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 194 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 195 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 196 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 197 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 198 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 199 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 200 | github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= 201 | github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= 202 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 203 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 204 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 205 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 206 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 207 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 208 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 209 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 210 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 211 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 212 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 213 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 214 | github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= 215 | github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 216 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 217 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 218 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 219 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 220 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 221 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 222 | github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= 223 | github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= 224 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 225 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 226 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 227 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 228 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 229 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 230 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 231 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 232 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 233 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 234 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 235 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 236 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 237 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 238 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= 239 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 240 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 241 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 242 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 243 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 244 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 245 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 246 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 247 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 248 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 249 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 250 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 251 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 252 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 253 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 254 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 255 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 256 | golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= 257 | golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 258 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 259 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 260 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 261 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 262 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 263 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 264 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 265 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 266 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= 267 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 268 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 269 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 270 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 271 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 272 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 273 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 274 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 275 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 276 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 277 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 278 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 279 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 280 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 281 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 282 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 283 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 284 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 285 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 286 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 287 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 288 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 289 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 290 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 291 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 292 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 293 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 294 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 295 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 296 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 297 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 298 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 299 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 300 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 301 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 302 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 303 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 304 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 305 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 306 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 307 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 308 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 309 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 310 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 311 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 312 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 313 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 314 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 315 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 316 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 317 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 318 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 319 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 320 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 321 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 322 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 323 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 324 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 325 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 326 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 327 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 328 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 329 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 330 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 331 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 332 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 333 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 334 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 335 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 336 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 337 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 338 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 339 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 340 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 341 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 342 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 343 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 344 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 345 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 346 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 347 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 348 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 349 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 350 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 351 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 352 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 353 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 354 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 355 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 356 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 357 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 358 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 359 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 360 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 362 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 363 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 364 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 365 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 366 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 367 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 368 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 369 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 370 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 371 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 372 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 373 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 374 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 375 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 376 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 377 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 378 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 379 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 381 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 382 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 384 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 385 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 386 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 387 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 394 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 395 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 396 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 397 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 398 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 399 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 400 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 401 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 402 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 403 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 404 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 405 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 406 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 407 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 408 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 409 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 410 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 411 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 412 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 413 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 414 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 415 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 416 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 417 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 418 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 419 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 420 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 421 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 422 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 423 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 424 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 425 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 426 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 427 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 428 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 429 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 430 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 431 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 432 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 433 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 434 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 435 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 436 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 437 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 438 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 439 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 440 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 441 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 442 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 443 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 444 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 445 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 446 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 447 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 448 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 449 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 450 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 451 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 452 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 453 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 454 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 455 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 456 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 457 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 458 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 459 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 460 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 461 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 462 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 463 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 464 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 465 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 466 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 467 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 468 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 469 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 470 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 471 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 472 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 473 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 474 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 475 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 476 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 477 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 478 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 479 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 480 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 481 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 482 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 483 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 484 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 485 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 486 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 487 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 488 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 489 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 490 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 491 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 492 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 493 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 494 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 495 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 496 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 497 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 498 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 499 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 500 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 501 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 502 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 503 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 504 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 505 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 506 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 507 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 508 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 509 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 510 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 511 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 512 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 513 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 514 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 515 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 516 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 517 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 518 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 519 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 520 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 521 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 522 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 523 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 524 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 525 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 526 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 527 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 528 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 529 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 530 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 531 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 532 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 533 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 534 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 535 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 536 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 537 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 538 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 539 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 540 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 541 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 542 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 543 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 544 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 545 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 546 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 547 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 548 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 549 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 550 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 551 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 552 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 553 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 554 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 555 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 556 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 557 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 558 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 559 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 560 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 561 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 562 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 563 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 564 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 565 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 566 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 567 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 568 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 569 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 570 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 571 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 572 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 573 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 574 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 575 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 576 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 577 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 578 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 579 | --------------------------------------------------------------------------------