├── .gitignore ├── db ├── migration │ ├── 000003_add_sessions.down.sql │ ├── 000001_init_schema.down.sql │ ├── 000002_add_users.down.sql │ ├── 000003_add_sessions.up.sql │ ├── 000002_add_users.up.sql │ └── 000001_init_schema.up.sql ├── query │ ├── user.sql │ ├── session.sql │ ├── entry.sql │ ├── transfer.sql │ └── account.sql ├── sqlc │ ├── main_test.go │ ├── db.go │ ├── user.sql.go │ ├── models.go │ ├── querier.go │ ├── user_test.go │ ├── session.sql.go │ ├── entry_test.go │ ├── entry.sql.go │ ├── account_test.go │ ├── transfer_test.go │ ├── transfer.sql.go │ ├── account.sql.go │ ├── store.go │ └── store_test.go ├── dbdiagram.io │ ├── simple_bank.sql │ └── simple_bank-2.sql ├── mockdb │ └── store.go └── mock │ └── store.go ├── startup.sh ├── app.env ├── sqlc.yaml ├── api ├── validator.go ├── main_test.go ├── middleware.go ├── server.go ├── token.go ├── transfer.go ├── middleware_test.go ├── account_test.go ├── user.go ├── account.go └── user_test.go ├── util ├── currency.go ├── password.go ├── password_test.go ├── config.go └── random.go ├── token ├── maker.go ├── payload.go ├── paseto_maker_test.go ├── paseto_maker.go ├── jwt_maker.go └── jwt_maker_test.go ├── Dockerfile ├── docker-compose.yml ├── main.go ├── Makefile ├── .github └── workflows │ └── test.yml ├── go.mod ├── README.md ├── wait-for.sh └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | vendor 3 | .vscode -------------------------------------------------------------------------------- /db/migration/000003_add_sessions.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS "sessions"; -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "run db migration" 6 | /app/migrate -path /app/migration -database "$DB_SOURCE" -verbose up 7 | 8 | echo "start the app" 9 | exec "$@" -------------------------------------------------------------------------------- /db/migration/000002_add_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE IF EXISTS "accounts" DROP CONSTRAINT IF EXISTS "owner_currency_key"; 2 | 3 | ALTER TABLE IF EXISTS "accounts" DROP CONSTRAINT IF EXISTS "accounts_owner_fkey"; 4 | 5 | DROP TABLE IF EXISTS "users"; -------------------------------------------------------------------------------- /app.env: -------------------------------------------------------------------------------- 1 | DB_DRIVER=postgres 2 | DB_SOURCE=postgres://root:secret@localhost:5432/simple_bank?sslmode=disable 3 | SERVER_ADDRESS=0.0.0.0:8080 4 | TOKEN_SYMMETRIC_KEY=12345678901234567890123456789012 5 | ACCESS_TOKEN_DURATION=15m 6 | REFRESH_TOKEN_DURATION=24h -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '1' 2 | packages: 3 | - name: 'db' 4 | path: './db/sqlc' 5 | queries: './db/query/' 6 | schema: './db/migration/' 7 | engine: 'postgresql' 8 | emit_json_tags: true 9 | emit_prepared_queries: false 10 | emit_interface: true 11 | emit_exact_table_names: false 12 | emit_empty_slices: true 13 | -------------------------------------------------------------------------------- /db/query/session.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateSession :one 2 | INSERT INTO sessions ( 3 | id, 4 | username, 5 | refresh_token, 6 | user_agent, 7 | client_ip, 8 | is_blocked, 9 | expires_at 10 | ) VALUES ( 11 | $1, $2, $3, $4, $5, $6, $7 12 | ) RETURNING *; 13 | 14 | -- name: GetSession :one 15 | SELECT * FROM sessions 16 | WHERE id = $1 LIMIT 1; -------------------------------------------------------------------------------- /api/validator.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "github.com/samirprakash/go-bank/util" 6 | ) 7 | 8 | var validateCurrency 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 | -------------------------------------------------------------------------------- /util/currency.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // Constants for all supported currencies 4 | const ( 5 | USD = "USD" 6 | EUR = "EUR" 7 | GBP = "GBP" 8 | INR = "INR" 9 | ) 10 | 11 | // IsSupportedCurrency checks is a currency is supported or not 12 | func IsSupportedCurrency(currency string) bool { 13 | switch currency { 14 | case USD, EUR, GBP, INR: 15 | return true 16 | } 17 | return false 18 | } 19 | -------------------------------------------------------------------------------- /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 creates a new token for a specific username and duration 8 | CreateToken(username string, duration time.Duration) (string, *Payload, error) 9 | 10 | // VerifyToken verifies if the token is valid or not 11 | VerifyToken(token string) (*Payload, error) 12 | } 13 | -------------------------------------------------------------------------------- /db/migration/000003_add_sessions.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "sessions" ( 2 | "id" uuid PRIMARY KEY, 3 | "username" varchar NOT NULL, 4 | "refresh_token" varchar NOT NULL, 5 | "user_agent" varchar NOT NULL, 6 | "client_ip" varchar NOT NULL, 7 | "is_blocked" boolean NOT NULL DEFAULT false, 8 | "expires_at" timestamptz NOT NULL, 9 | "created_at" timestamptz NOT NULL DEFAULT (now()) 10 | ); 11 | 12 | ALTER TABLE "sessions" ADD FOREIGN KEY ("username") REFERENCES "users" ("username"); -------------------------------------------------------------------------------- /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 | ALTER TABLE "accounts" ADD CONSTRAINT "owner_currency_key" UNIQUE ("owner", "currency"); -------------------------------------------------------------------------------- /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 | ORDER BY id 16 | LIMIT $1 17 | OFFSET $2; 18 | 19 | -- name: UpdateEntry :one 20 | Update entries 21 | SET amount = $2 22 | WHERE id = $1 23 | RETURNING *; 24 | 25 | -- name: DeleteEntry :exec 26 | DELETE FROM entries 27 | WHERE id = $1; -------------------------------------------------------------------------------- /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 | ORDER BY id 17 | LIMIT $1 18 | OFFSET $2; 19 | 20 | -- name: UpdateTransfer :one 21 | Update transfers 22 | SET amount = $2 23 | WHERE id = $1 24 | RETURNING *; 25 | 26 | -- name: DeleteTransfer :exec 27 | DELETE FROM transfers 28 | WHERE id = $1; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM golang:1.20.3-alpine3.17 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.14.1/migrate.linux-amd64.tar.gz | tar xvz 8 | 9 | # run stage 10 | FROM alpine:3.17 11 | WORKDIR /app 12 | COPY --from=builder /app/main . 13 | COPY --from=builder /app/migrate.linux-amd64 ./migrate 14 | COPY app.env . 15 | COPY startup.sh . 16 | COPY wait-for.sh . 17 | COPY db/migration ./migration 18 | 19 | EXPOSE 8080 20 | ENTRYPOINT [ "/app/startup.sh" ] 21 | CMD [ "/app/main" ] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 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=postgres://root:secret@postgres:5432/simple_bank?sslmode=disable 17 | depends_on: 18 | - postgres 19 | entrypoint: ['/app/wait-for.sh', 'postgres:5432', '--', '/app/startup.sh'] 20 | command: ['/app/main'] 21 | -------------------------------------------------------------------------------- /db/sqlc/main_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | _ "github.com/lib/pq" 10 | "github.com/samirprakash/go-bank/util" 11 | ) 12 | 13 | var testQueries *Queries 14 | var testDB *sql.DB 15 | 16 | func TestMain(m *testing.M) { 17 | config, err := util.LoadConfig("../..") 18 | if err != nil { 19 | log.Fatal("Not able to load config", err) 20 | } 21 | 22 | testDB, err = sql.Open(config.DBDriver, config.DBSource) 23 | if err != nil { 24 | log.Fatal("cannot connect to database", err) 25 | } 26 | 27 | testQueries = New(testDB) 28 | 29 | os.Exit(m.Run()) 30 | } 31 | -------------------------------------------------------------------------------- /api/main_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | db "github.com/samirprakash/go-bank/db/sqlc" 10 | "github.com/samirprakash/go-bank/util" 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 | 20 | server, err := NewServer(config, store) 21 | require.NoError(t, err) 22 | 23 | return server 24 | } 25 | 26 | func TestMain(m *testing.M) { 27 | gin.SetMode(gin.TestMode) 28 | os.Exit(m.Run()) 29 | } 30 | -------------------------------------------------------------------------------- /util/password.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/crypto/bcrypt" 7 | ) 8 | 9 | // HashPassword generates a brypt hash for 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 | 16 | return string(hashedPassword), nil 17 | } 18 | 19 | // CheckPassword validates if the password is correct or not 20 | func CheckPassword(password, hashedPassword string) error { 21 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 22 | } 23 | -------------------------------------------------------------------------------- /db/sqlc/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.13.0 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | hashedhPassword1, err := HashPassword(password) 14 | require.NoError(t, err) 15 | require.NotEmpty(t, hashedhPassword1) 16 | 17 | err = CheckPassword(password, hashedhPassword1) 18 | require.NoError(t, err) 19 | 20 | wrongPassword := RandomString(6) 21 | err = CheckPassword(wrongPassword, hashedhPassword1) 22 | require.EqualError(t, err, bcrypt.ErrMismatchedHashAndPassword.Error()) 23 | 24 | hashedhPassword2, err := HashPassword(password) 25 | require.NoError(t, err) 26 | require.NotEmpty(t, hashedhPassword2) 27 | require.NotEqual(t, hashedhPassword1, hashedhPassword2) 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: UpdateAccountBalance :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/golang/mock/mockgen/model" 8 | _ "github.com/lib/pq" 9 | "github.com/samirprakash/go-bank/api" 10 | db "github.com/samirprakash/go-bank/db/sqlc" 11 | "github.com/samirprakash/go-bank/util" 12 | ) 13 | 14 | func main() { 15 | // load config from file or env vars using viper 16 | config, err := util.LoadConfig(".") 17 | if err != nil { 18 | log.Fatal("Not able to load config", err) 19 | } 20 | 21 | // connect to the database 22 | conn, err := sql.Open(config.DBDriver, config.DBSource) 23 | if err != nil { 24 | log.Fatal("cannot connect to database", err) 25 | } 26 | 27 | // create a database store for executing queries 28 | store := db.NewStore(conn) 29 | // create a server connected to the store 30 | server, err := api.NewServer(config, store) 31 | if err != nil { 32 | log.Fatal("cannot create server : ", err) 33 | } 34 | 35 | // start the server 36 | err = server.Start(config.ServerAddress) 37 | if err != nil { 38 | log.Fatal("cannot start server", err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /token/payload.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | var ErrInvalidToken = errors.New("token is invalid") 11 | var ErrExpiredToken = errors.New("token has expired") 12 | 13 | // Payload contains the payload data for the token 14 | type Payload struct { 15 | ID uuid.UUID `json:"id"` 16 | Username string `json:"username"` 17 | IssuedAt time.Time `json:"issued_at"` 18 | ExpiredAt time.Time `json:"expired_at"` 19 | } 20 | 21 | // NewPayload creates a new token payload with a specific username and duration 22 | func NewPayload(username string, duration time.Duration) (*Payload, error) { 23 | 24 | tokenID, err := uuid.NewRandom() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | payload := &Payload{ 30 | ID: tokenID, 31 | Username: username, 32 | IssuedAt: time.Now(), 33 | ExpiredAt: time.Now().Add(duration), 34 | } 35 | 36 | return payload, err 37 | } 38 | 39 | func (payload *Payload) Valid() error { 40 | if time.Now().After(payload.ExpiredAt) { 41 | return ErrExpiredToken 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /util/config.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // Config stores the confifguration for the app 10 | // Values are read by Viper from a config file or from an environment varible 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_DURATION"` 17 | RefreshTokenDuration time.Duration `mapstructure:"REFRESH_TOKEN_DURATION"` 18 | } 19 | 20 | // LoadConfig loads the configuration from an config file or from environment vars 21 | func LoadConfig(path string) (config Config, err error) { 22 | viper.AddConfigPath(path) 23 | viper.SetConfigName("app") 24 | viper.SetConfigType("env") 25 | 26 | // override env vars over config file values 27 | viper.AutomaticEnv() 28 | 29 | err = viper.ReadInConfig() 30 | if err != nil { 31 | return 32 | } 33 | 34 | // load config struct with values and return 35 | err = viper.Unmarshal(&config) 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | postgres: 2 | docker run --name go-bank --network bank-network -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:14-alpine 3 | 4 | createdb: 5 | docker exec -it go-bank createdb --username=root --owner=root simple_bank 6 | 7 | dropdb: 8 | docker exec -it go-bank dropdb simple_bank 9 | 10 | migrateup: 11 | migrate -path db/migration -database "postgres://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose up 12 | 13 | migratedown: 14 | migrate -path db/migration -database "postgres://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose down 15 | 16 | sqlc: 17 | sqlc generate 18 | 19 | test: 20 | go test -v -cover ./... 21 | 22 | tidy: 23 | go mod tidy 24 | 25 | vendor: 26 | go mod vendor 27 | 28 | server: 29 | go run main.go 30 | 31 | mock: 32 | mockgen -package mockdb -destination db/mock/store.go github.com/samirprakash/go-bank/db/sqlc Store 33 | 34 | local: 35 | docker build -t gobank:latest . 36 | docker run --name gobank --network bank-network -p 8080:8080 -e GIN_MODE=release -e DB_SOURCE="postgres://root:secret@go-bank:5432/simple_bank?sslmode=disable" gobank:latest 37 | 38 | .PHONY: postgres createdb dropdb migrateup migratedown migrateup1 migratedown1 sqlc test server mock local -------------------------------------------------------------------------------- /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 | const alphabet = "abcdefghhijklmnopqrstuvwxyz" 15 | 16 | // RandomInt generates a random integer between min and max 17 | func RandomInt(min, max int64) int64 { 18 | return min + rand.Int63n(max-min+1) 19 | } 20 | 21 | // RandomString geerates a random string of length n 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 | // RandomOwnerName generates random account owner names for testing 35 | func RandomOwnerName() string { 36 | return RandomString(6) 37 | } 38 | 39 | // RandomAmount generates random amount to be used as balance for testing 40 | func RandomAmount() int64 { 41 | return RandomInt(0, 1000) 42 | } 43 | 44 | // RandomCurrency returns one of the provided values from the provided slice of currencies 45 | func RandomCurrency() string { 46 | currencies := []string{"EUR", "USD", "GBP", "INR"} 47 | n := len(currencies) 48 | return currencies[rand.Intn(n)] 49 | } 50 | 51 | // RandomEmail generates a random email address 52 | func RandomEmail() string { 53 | return fmt.Sprintf("%s@email.com", RandomString(6)) 54 | } 55 | -------------------------------------------------------------------------------- /db/dbdiagram.io/simple_bank.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 | ALTER TABLE "entries" 25 | ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id"); 26 | 27 | ALTER TABLE "transfers" 28 | ADD FOREIGN KEY ("from_account_id") REFERENCES "accounts" ("id"); 29 | 30 | ALTER TABLE "transfers" 31 | ADD FOREIGN KEY ("to_account_id") REFERENCES "accounts" ("id"); 32 | 33 | CREATE INDEX ON "accounts" ("owner"); 34 | 35 | CREATE INDEX ON "entries" ("account_id"); 36 | 37 | CREATE INDEX ON "transfers" ("from_account_id"); 38 | 39 | CREATE INDEX ON "transfers" ("to_account_id"); 40 | 41 | CREATE INDEX ON "transfers" ("from_account_id", "to_account_id"); 42 | 43 | COMMENT ON COLUMN "entries"."amount" IS 'can be negative or positive'; 44 | 45 | COMMENT ON COLUMN "transfers"."amount" IS 'must be positive'; -------------------------------------------------------------------------------- /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 | ALTER TABLE "entries" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id"); 25 | 26 | ALTER TABLE "transfers" ADD FOREIGN KEY ("from_account_id") REFERENCES "accounts" ("id"); 27 | 28 | ALTER TABLE "transfers" ADD FOREIGN KEY ("to_account_id") REFERENCES "accounts" ("id"); 29 | 30 | CREATE INDEX ON "accounts" ("owner"); 31 | 32 | CREATE INDEX ON "entries" ("account_id"); 33 | 34 | CREATE INDEX ON "transfers" ("from_account_id"); 35 | 36 | CREATE INDEX ON "transfers" ("to_account_id"); 37 | 38 | CREATE INDEX ON "transfers" ("from_account_id", "to_account_id"); 39 | 40 | COMMENT ON COLUMN "entries"."amount" IS 'can be negative or positive'; 41 | 42 | COMMENT ON COLUMN "transfers"."amount" IS 'must be positive'; -------------------------------------------------------------------------------- /.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 Code 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | postgres: 16 | image: postgres:14-alpine 17 | env: 18 | POSTGRES_USER: root 19 | POSTGRES_PASSWORD: secret 20 | POSTGRES_DB: simple_bank 21 | ports: 22 | - 5432:5432 23 | options: >- 24 | --health-cmd pg_isready 25 | --health-interval 10s 26 | --health-timeout 5s 27 | --health-retries 5 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v2 32 | 33 | - name: Set up Go 34 | uses: actions/setup-go@v2 35 | with: 36 | go-version: 1.18 37 | id: go 38 | 39 | - name: Install migrate CLI 40 | run: | 41 | curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz 42 | sudo mv migrate.linux-amd64 /usr/bin/migrate 43 | which migrate 44 | 45 | - name: Run migrations 46 | run: make migrateup 47 | 48 | - name: Tidy dependencies 49 | run: make tidy 50 | 51 | - name: Update vendor 52 | run: make vendor 53 | 54 | - name: Run tests 55 | run: make test 56 | -------------------------------------------------------------------------------- /token/paseto_maker_test.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/samirprakash/go-bank/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.RandomOwnerName() 16 | duration := time.Minute 17 | 18 | issuedAt := time.Now() 19 | expiredAt := issuedAt.Add(duration) 20 | 21 | token, payload, err := maker.CreateToken(username, duration) 22 | require.NoError(t, err) 23 | require.NotEmpty(t, token) 24 | require.NotEmpty(t, payload) 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 | func TestExpiredPasetoToken(t *testing.T) { 37 | maker, err := NewPasetoMaker(util.RandomString(32)) 38 | require.NoError(t, err) 39 | require.NotEmpty(t, maker) 40 | 41 | token, payload, err := maker.CreateToken(util.RandomOwnerName(), -time.Minute) 42 | require.NoError(t, err) 43 | require.NotEmpty(t, token) 44 | require.NotEmpty(t, payload) 45 | 46 | payload, err = maker.VerifyToken(token) 47 | require.Error(t, err) 48 | require.EqualError(t, err, ErrExpiredToken.Error()) 49 | require.Nil(t, payload) 50 | } 51 | -------------------------------------------------------------------------------- /api/middleware.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/samirprakash/go-bank/token" 11 | ) 12 | 13 | const ( 14 | authorizationHeaderKey = "authorization" 15 | authorizationTypeBearer = "bearer" 16 | authorizationPayloadKey = "authrorization_payload" 17 | ) 18 | 19 | func authMiddleware(tokenMaker token.Maker) gin.HandlerFunc { 20 | return func(ctx *gin.Context) { 21 | authorizationHeader := ctx.GetHeader(authorizationHeaderKey) 22 | if len(authorizationHeader) == 0 { 23 | err := errors.New("authorization header not provided") 24 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err)) 25 | return 26 | } 27 | 28 | fields := strings.Fields(authorizationHeader) 29 | if len(fields) < 2 { 30 | err := errors.New("invalid authorization header format") 31 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err)) 32 | return 33 | } 34 | 35 | authorizationType := strings.ToLower(fields[0]) 36 | if authorizationType != authorizationTypeBearer { 37 | err := fmt.Errorf("unsupported token type %s", authorizationType) 38 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err)) 39 | return 40 | } 41 | 42 | accessToken := fields[1] 43 | payload, err := tokenMaker.VerifyToken(accessToken) 44 | if err != nil { 45 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err)) 46 | return 47 | } 48 | 49 | ctx.Set(authorizationPayloadKey, payload) 50 | ctx.Next() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /token/paseto_maker.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/o1egl/paseto" 8 | "golang.org/x/crypto/chacha20poly1305" 9 | ) 10 | 11 | // PasetoMaker is a PASETO token maker 12 | type PasetoMaker struct { 13 | paseto *paseto.V2 14 | symmetricrKey []byte 15 | } 16 | 17 | // NewPasetoMaker creates a new PasetoMaker instance 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 | symmetricrKey: []byte(symmetricKey), 26 | } 27 | 28 | return maker, nil 29 | } 30 | 31 | // CreateToken creates a new token for a specific username and duration 32 | func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, *Payload, error) { 33 | payload, err := NewPayload(username, duration) 34 | if err != nil { 35 | return "", payload, err 36 | } 37 | 38 | token, err := maker.paseto.Encrypt(maker.symmetricrKey, payload, nil) 39 | return token, payload, err 40 | } 41 | 42 | // VerifyToken verifies if the token is valid or not 43 | func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) { 44 | payload := &Payload{} 45 | 46 | err := maker.paseto.Decrypt(token, maker.symmetricrKey, payload, nil) 47 | if err != nil { 48 | return nil, ErrInvalidToken 49 | } 50 | 51 | err = payload.Valid() 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return payload, nil 57 | } 58 | -------------------------------------------------------------------------------- /db/sqlc/user.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.13.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.db.QueryRowContext(ctx, 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.db.QueryRowContext(ctx, 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/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.13.0 4 | 5 | package db 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type Account struct { 14 | ID int64 `json:"id"` 15 | Owner string `json:"owner"` 16 | Balance int64 `json:"balance"` 17 | Currency string `json:"currency"` 18 | CreatedAt time.Time `json:"created_at"` 19 | } 20 | 21 | type Entry struct { 22 | ID int64 `json:"id"` 23 | AccountID int64 `json:"account_id"` 24 | // can be negative or positive 25 | Amount int64 `json:"amount"` 26 | CreatedAt time.Time `json:"created_at"` 27 | } 28 | 29 | type Session struct { 30 | ID uuid.UUID `json:"id"` 31 | Username string `json:"username"` 32 | RefreshToken string `json:"refresh_token"` 33 | UserAgent string `json:"user_agent"` 34 | ClientIp string `json:"client_ip"` 35 | IsBlocked bool `json:"is_blocked"` 36 | ExpiresAt time.Time `json:"expires_at"` 37 | CreatedAt time.Time `json:"created_at"` 38 | } 39 | 40 | type Transfer struct { 41 | ID int64 `json:"id"` 42 | FromAccountID int64 `json:"from_account_id"` 43 | ToAccountID int64 `json:"to_account_id"` 44 | // must be positive 45 | Amount int64 `json:"amount"` 46 | CreatedAt time.Time `json:"created_at"` 47 | } 48 | 49 | type User struct { 50 | Username string `json:"username"` 51 | HashedPassword string `json:"hashed_password"` 52 | FullName string `json:"full_name"` 53 | Email string `json:"email"` 54 | PasswordChangedAt time.Time `json:"password_changed_at"` 55 | CreatedAt time.Time `json:"created_at"` 56 | } 57 | -------------------------------------------------------------------------------- /db/sqlc/querier.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.13.0 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type Querier interface { 14 | CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) 15 | CreateEntry(ctx context.Context, arg CreateEntryParams) (Entry, error) 16 | CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) 17 | CreateTransfer(ctx context.Context, arg CreateTransferParams) (Transfer, error) 18 | CreateUser(ctx context.Context, arg CreateUserParams) (User, error) 19 | DeleteAccount(ctx context.Context, id int64) error 20 | DeleteEntry(ctx context.Context, id int64) error 21 | DeleteTransfer(ctx context.Context, id int64) error 22 | GetAccount(ctx context.Context, id int64) (Account, error) 23 | GetAccountForUpdate(ctx context.Context, id int64) (Account, error) 24 | GetEntry(ctx context.Context, id int64) (Entry, error) 25 | GetSession(ctx context.Context, id uuid.UUID) (Session, error) 26 | GetTransfer(ctx context.Context, id int64) (Transfer, error) 27 | GetUser(ctx context.Context, username string) (User, error) 28 | ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) 29 | ListEntries(ctx context.Context, arg ListEntriesParams) ([]Entry, error) 30 | ListTransfers(ctx context.Context, arg ListTransfersParams) ([]Transfer, error) 31 | UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) 32 | UpdateAccountBalance(ctx context.Context, arg UpdateAccountBalanceParams) (Account, error) 33 | UpdateEntry(ctx context.Context, arg UpdateEntryParams) (Entry, error) 34 | UpdateTransfer(ctx context.Context, arg UpdateTransferParams) (Transfer, error) 35 | } 36 | 37 | var _ Querier = (*Queries)(nil) 38 | -------------------------------------------------------------------------------- /db/sqlc/user_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/samirprakash/go-bank/util" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | // createRandomUser creates a random user to be used in tests 13 | func createRandomUser(t *testing.T) User { 14 | // generate a hashed password 15 | hashedPassword, err := util.HashPassword(util.RandomString(6)) 16 | require.NoError(t, err) 17 | require.NotEmpty(t, hashedPassword) 18 | 19 | arg := CreateUserParams{ 20 | Username: util.RandomOwnerName(), 21 | HashedPassword: hashedPassword, 22 | FullName: util.RandomOwnerName(), 23 | Email: util.RandomEmail(), 24 | } 25 | 26 | user, err := testQueries.CreateUser(context.Background(), arg) 27 | require.NoError(t, err) 28 | require.NotEmpty(t, user) 29 | 30 | require.Equal(t, arg.Username, user.Username) 31 | require.Equal(t, arg.HashedPassword, user.HashedPassword) 32 | require.Equal(t, arg.FullName, user.FullName) 33 | require.Equal(t, arg.Email, user.Email) 34 | 35 | require.True(t, user.PasswordChangedAt.IsZero()) 36 | require.NotZero(t, user.CreatedAt) 37 | 38 | return user 39 | } 40 | 41 | func TestCreateUser(t *testing.T) { 42 | createRandomUser(t) 43 | } 44 | 45 | func TestGetUser(t *testing.T) { 46 | user1 := createRandomUser(t) 47 | 48 | user2, err := testQueries.GetUser(context.Background(), user1.Username) 49 | require.NoError(t, err) 50 | require.NotEmpty(t, user2) 51 | 52 | require.Equal(t, user1.Username, user2.Username) 53 | require.Equal(t, user1.HashedPassword, user2.HashedPassword) 54 | require.Equal(t, user1.FullName, user2.FullName) 55 | require.Equal(t, user1.Email, user2.Email) 56 | 57 | require.WithinDuration(t, user1.PasswordChangedAt, user2.PasswordChangedAt, time.Second) 58 | require.WithinDuration(t, user1.CreatedAt, user2.CreatedAt, time.Second) 59 | } 60 | -------------------------------------------------------------------------------- /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 with the provided secretKey 19 | func NewJWTMaker(secretKey string) (Maker, error) { 20 | if len(secretKey) < minSecretKeysize { 21 | return nil, fmt.Errorf("invalid key size : must be %d characters", minSecretKeysize) 22 | } 23 | 24 | return &JWTMaker{secretKey}, nil 25 | } 26 | 27 | // CreateToken creates a new JSON web token for a specific username and duration 28 | func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, *Payload, error) { 29 | payload, err := NewPayload(username, duration) 30 | if err != nil { 31 | return "", payload, err 32 | } 33 | 34 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) 35 | token, err := jwtToken.SignedString([]byte(maker.secretKey)) 36 | 37 | return token, payload, err 38 | } 39 | 40 | // VerifyToken verifies if the JSON web token is valid or not 41 | func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) { 42 | keyFunc := func(token *jwt.Token) (interface{}, error) { 43 | _, ok := token.Method.(*jwt.SigningMethodHMAC) 44 | if !ok { 45 | return nil, ErrInvalidToken 46 | } 47 | return []byte(maker.secretKey), nil 48 | } 49 | 50 | jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc) 51 | if err != nil { 52 | verr, ok := err.(*jwt.ValidationError) 53 | if ok && errors.Is(verr.Inner, ErrExpiredToken) { 54 | return nil, ErrExpiredToken 55 | } 56 | return nil, ErrInvalidToken 57 | } 58 | 59 | payload, ok := jwtToken.Claims.(*Payload) 60 | if !ok { 61 | return nil, ErrInvalidToken 62 | } 63 | return payload, nil 64 | } 65 | -------------------------------------------------------------------------------- /db/dbdiagram.io/simple_bank-2.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 | CREATE TABLE "accounts" ( 11 | "id" bigserial PRIMARY KEY, 12 | "owner" varchar NOT NULL, 13 | "balance" bigint NOT NULL, 14 | "currency" varchar NOT NULL, 15 | "created_at" timestamptz NOT NULL DEFAULT (now()) 16 | ); 17 | 18 | CREATE TABLE "entries" ( 19 | "id" bigserial PRIMARY KEY, 20 | "account_id" bigint NOT NULL, 21 | "amount" bigint NOT NULL, 22 | "created_at" timestamptz NOT NULL DEFAULT (now()) 23 | ); 24 | 25 | CREATE TABLE "transfers" ( 26 | "id" bigserial PRIMARY KEY, 27 | "from_account_id" bigint NOT NULL, 28 | "to_account_id" bigint NOT NULL, 29 | "amount" bigint NOT NULL, 30 | "created_at" timestamptz NOT NULL DEFAULT (now()) 31 | ); 32 | 33 | ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username"); 34 | 35 | ALTER TABLE "entries" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id"); 36 | 37 | ALTER TABLE "transfers" ADD FOREIGN KEY ("from_account_id") REFERENCES "accounts" ("id"); 38 | 39 | ALTER TABLE "transfers" ADD FOREIGN KEY ("to_account_id") REFERENCES "accounts" ("id"); 40 | 41 | CREATE INDEX ON "accounts" ("owner"); 42 | 43 | CREATE INDEX ON "accounts" ("owner", "currency"); 44 | 45 | CREATE INDEX ON "entries" ("account_id"); 46 | 47 | CREATE INDEX ON "transfers" ("from_account_id"); 48 | 49 | CREATE INDEX ON "transfers" ("to_account_id"); 50 | 51 | CREATE INDEX ON "transfers" ("from_account_id", "to_account_id"); 52 | 53 | COMMENT ON COLUMN "entries"."amount" IS 'can be negative or positive'; 54 | 55 | COMMENT ON COLUMN "transfers"."amount" IS 'must be positive'; 56 | -------------------------------------------------------------------------------- /db/sqlc/session.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.13.0 4 | // source: session.sql 5 | 6 | package db 7 | 8 | import ( 9 | "context" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | ) 14 | 15 | const createSession = `-- name: CreateSession :one 16 | INSERT INTO sessions ( 17 | id, 18 | username, 19 | refresh_token, 20 | user_agent, 21 | client_ip, 22 | is_blocked, 23 | expires_at 24 | ) VALUES ( 25 | $1, $2, $3, $4, $5, $6, $7 26 | ) RETURNING id, username, refresh_token, user_agent, client_ip, is_blocked, expires_at, created_at 27 | ` 28 | 29 | type CreateSessionParams struct { 30 | ID uuid.UUID `json:"id"` 31 | Username string `json:"username"` 32 | RefreshToken string `json:"refresh_token"` 33 | UserAgent string `json:"user_agent"` 34 | ClientIp string `json:"client_ip"` 35 | IsBlocked bool `json:"is_blocked"` 36 | ExpiresAt time.Time `json:"expires_at"` 37 | } 38 | 39 | func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { 40 | row := q.db.QueryRowContext(ctx, createSession, 41 | arg.ID, 42 | arg.Username, 43 | arg.RefreshToken, 44 | arg.UserAgent, 45 | arg.ClientIp, 46 | arg.IsBlocked, 47 | arg.ExpiresAt, 48 | ) 49 | var i Session 50 | err := row.Scan( 51 | &i.ID, 52 | &i.Username, 53 | &i.RefreshToken, 54 | &i.UserAgent, 55 | &i.ClientIp, 56 | &i.IsBlocked, 57 | &i.ExpiresAt, 58 | &i.CreatedAt, 59 | ) 60 | return i, err 61 | } 62 | 63 | const getSession = `-- name: GetSession :one 64 | SELECT id, username, refresh_token, user_agent, client_ip, is_blocked, expires_at, created_at FROM sessions 65 | WHERE id = $1 LIMIT 1 66 | ` 67 | 68 | func (q *Queries) GetSession(ctx context.Context, id uuid.UUID) (Session, error) { 69 | row := q.db.QueryRowContext(ctx, getSession, id) 70 | var i Session 71 | err := row.Scan( 72 | &i.ID, 73 | &i.Username, 74 | &i.RefreshToken, 75 | &i.UserAgent, 76 | &i.ClientIp, 77 | &i.IsBlocked, 78 | &i.ExpiresAt, 79 | &i.CreatedAt, 80 | ) 81 | return i, err 82 | } 83 | -------------------------------------------------------------------------------- /token/jwt_maker_test.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/dgrijalva/jwt-go" 8 | "github.com/samirprakash/go-bank/util" 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.RandomOwnerName() 17 | duration := time.Minute 18 | 19 | issuedAt := time.Now() 20 | expiredAt := issuedAt.Add(duration) 21 | 22 | token, payload, err := maker.CreateToken(username, duration) 23 | require.NoError(t, err) 24 | require.NotEmpty(t, token) 25 | require.NotEmpty(t, payload) 26 | 27 | payload, err = maker.VerifyToken(token) 28 | require.NoError(t, err) 29 | require.NotEmpty(t, payload) 30 | 31 | require.NotZero(t, payload.ID) 32 | require.Equal(t, username, payload.Username) 33 | require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) 34 | require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) 35 | } 36 | 37 | func TestExpiredJWTToken(t *testing.T) { 38 | maker, err := NewJWTMaker(util.RandomString(32)) 39 | require.NoError(t, err) 40 | require.NotEmpty(t, maker) 41 | 42 | token, payload, err := maker.CreateToken(util.RandomOwnerName(), -time.Minute) 43 | require.NoError(t, err) 44 | require.NotEmpty(t, token) 45 | require.NotEmpty(t, payload) 46 | 47 | payload, err = maker.VerifyToken(token) 48 | require.Error(t, err) 49 | require.EqualError(t, err, ErrExpiredToken.Error()) 50 | require.Nil(t, payload) 51 | } 52 | 53 | func TestInvalidTokenAlgNone(t *testing.T) { 54 | payload, err := NewPayload(util.RandomOwnerName(), time.Minute) 55 | require.NoError(t, err) 56 | 57 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, payload) 58 | token, err := jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType) 59 | require.NoError(t, err) 60 | 61 | maker, err := NewJWTMaker(util.RandomString(32)) 62 | require.NoError(t, err) 63 | 64 | payload, err = maker.VerifyToken(token) 65 | require.Error(t, err) 66 | require.EqualError(t, err, ErrInvalidToken.Error()) 67 | require.Nil(t, payload) 68 | } 69 | -------------------------------------------------------------------------------- /api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/gin-gonic/gin/binding" 8 | "github.com/go-playground/validator/v10" 9 | db "github.com/samirprakash/go-bank/db/sqlc" 10 | "github.com/samirprakash/go-bank/token" 11 | "github.com/samirprakash/go-bank/util" 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 sets up 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("customCurrencyValidator", validateCurrency) 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 | router.POST("/tokens/renew_access", server.renewAccessToken) 49 | 50 | authRoutes := router.Group("/").Use(authMiddleware(server.tokenMaker)) 51 | 52 | authRoutes.POST("/accounts", server.createAccount) 53 | authRoutes.GET("/accounts/:id", server.getAccount) 54 | authRoutes.GET("/accounts", server.listAccounts) 55 | authRoutes.PATCH("/accounts/:id", server.updateAccountBalance) 56 | authRoutes.DELETE("/accounts/:id", server.deleteAccount) 57 | 58 | authRoutes.POST("/transfers", server.createTransfer) 59 | 60 | server.router = router 61 | } 62 | 63 | // Start starts a new server on the specified address 64 | func (server *Server) Start(address string) error { 65 | return server.router.Run(address) 66 | } 67 | 68 | // returns custom error messages 69 | func errorResponse(err error) gin.H { 70 | return gin.H{"error": err.Error()} 71 | } 72 | -------------------------------------------------------------------------------- /api/token.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type renewAccessTokenRequest struct { 13 | RefreshToken string `json:"refresh_token" binding:"required"` 14 | } 15 | 16 | type renewAccessTokenResponse struct { 17 | AccessToken string `json:"access_token"` 18 | AccessTokenExpiresAt time.Time `json:"access_token_expires_at"` 19 | } 20 | 21 | func (server *Server) renewAccessToken(ctx *gin.Context) { 22 | var req renewAccessTokenRequest 23 | 24 | if err := ctx.ShouldBindJSON(&req); err != nil { 25 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 26 | return 27 | } 28 | 29 | refreshPayload, err := server.tokenMaker.VerifyToken(req.RefreshToken) 30 | if err != nil { 31 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 32 | return 33 | } 34 | 35 | session, err := server.store.GetSession(ctx, refreshPayload.ID) 36 | if err != nil { 37 | if err == sql.ErrNoRows { 38 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 39 | return 40 | } 41 | 42 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 43 | return 44 | } 45 | 46 | if session.IsBlocked { 47 | err := fmt.Errorf("blocked session") 48 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 49 | return 50 | } 51 | 52 | if session.Username != refreshPayload.Username { 53 | err := fmt.Errorf("incorrect session user") 54 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 55 | return 56 | } 57 | 58 | if session.RefreshToken != req.RefreshToken { 59 | err := fmt.Errorf("mismatched session token") 60 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 61 | return 62 | } 63 | 64 | if time.Now().After(session.ExpiresAt) { 65 | err := fmt.Errorf("expired session") 66 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 67 | return 68 | } 69 | 70 | accessToken, accessPayload, err := server.tokenMaker.CreateToken(refreshPayload.Username, server.config.AccessTokenDuration) 71 | if err != nil { 72 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 73 | return 74 | } 75 | 76 | rsp := renewAccessTokenResponse{ 77 | AccessToken: accessToken, 78 | AccessTokenExpiresAt: accessPayload.ExpiredAt, 79 | } 80 | 81 | ctx.JSON(http.StatusOK, rsp) 82 | } 83 | -------------------------------------------------------------------------------- /api/transfer.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | db "github.com/samirprakash/go-bank/db/sqlc" 11 | "github.com/samirprakash/go-bank/token" 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,customCurrencyValidator"` 19 | } 20 | 21 | func (server *Server) createTransfer(ctx *gin.Context) { 22 | var req transferRequest 23 | 24 | if err := ctx.ShouldBindJSON(&req); err != nil { 25 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 26 | return 27 | } 28 | 29 | fromAccount, valid := server.validAccount(ctx, req.FromAccountID, req.Currency) 30 | if !valid { 31 | return 32 | } 33 | 34 | // add auth middleware 35 | authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) 36 | if fromAccount.Owner != authPayload.Username { 37 | err := errors.New("from account does not belong to the authenticated user") 38 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 39 | return 40 | } 41 | 42 | _, valid = server.validAccount(ctx, req.ToAccountID, req.Currency) 43 | if !valid { 44 | return 45 | } 46 | 47 | arg := db.TransferTxParams{ 48 | FromAccountID: req.FromAccountID, 49 | ToAccountID: req.ToAccountID, 50 | Amount: req.Amount, 51 | } 52 | 53 | result, err := server.store.TransferTx(ctx, arg) 54 | if err != nil { 55 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 56 | return 57 | } 58 | 59 | ctx.JSON(http.StatusOK, result) 60 | } 61 | 62 | func (server *Server) validAccount(ctx *gin.Context, accountID int64, currency string) (db.Account, bool) { 63 | account, err := server.store.GetAccount(ctx, accountID) 64 | if err != nil { 65 | if err == sql.ErrNoRows { 66 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 67 | return account, false 68 | } 69 | 70 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 71 | return account, false 72 | } 73 | 74 | if account.Currency != currency { 75 | err := fmt.Errorf("account [%d] current mismatch : %s vs %s", accountID, account.Currency, currency) 76 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 77 | return account, false 78 | } 79 | 80 | return account, true 81 | } 82 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/samirprakash/go-bank 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gin-gonic/gin v1.9.0 8 | github.com/go-playground/validator/v10 v10.13.0 9 | github.com/golang/mock v1.6.0 10 | github.com/google/uuid v1.3.0 11 | github.com/lib/pq v1.10.9 12 | github.com/o1egl/paseto v1.0.0 13 | github.com/spf13/viper v1.15.0 14 | github.com/stretchr/testify v1.8.2 15 | golang.org/x/crypto v0.8.0 16 | ) 17 | 18 | require ( 19 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect 20 | github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect 21 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect 22 | github.com/bytedance/sonic v1.8.8 // indirect 23 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/fsnotify/fsnotify v1.6.0 // indirect 26 | github.com/gin-contrib/sse v0.1.0 // indirect 27 | github.com/go-playground/locales v0.14.1 // indirect 28 | github.com/go-playground/universal-translator v0.18.1 // indirect 29 | github.com/goccy/go-json v0.10.2 // indirect 30 | github.com/hashicorp/hcl v1.0.0 // indirect 31 | github.com/json-iterator/go v1.1.12 // indirect 32 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 33 | github.com/leodido/go-urn v1.2.4 // indirect 34 | github.com/magiconair/properties v1.8.7 // indirect 35 | github.com/mattn/go-isatty v0.0.18 // indirect 36 | github.com/mitchellh/mapstructure v1.5.0 // indirect 37 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 38 | github.com/modern-go/reflect2 v1.0.2 // indirect 39 | github.com/pelletier/go-toml/v2 v2.0.7 // indirect 40 | github.com/pkg/errors v0.9.1 // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/spf13/afero v1.9.5 // indirect 43 | github.com/spf13/cast v1.5.0 // indirect 44 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 45 | github.com/spf13/pflag v1.0.5 // indirect 46 | github.com/subosito/gotenv v1.4.2 // indirect 47 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 48 | github.com/ugorji/go/codec v1.2.11 // indirect 49 | golang.org/x/arch v0.3.0 // indirect 50 | golang.org/x/net v0.9.0 // indirect 51 | golang.org/x/sys v0.7.0 // indirect 52 | golang.org/x/text v0.9.0 // indirect 53 | google.golang.org/protobuf v1.30.0 // indirect 54 | gopkg.in/ini.v1 v1.67.0 // indirect 55 | gopkg.in/yaml.v3 v3.0.1 // indirect 56 | ) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Simple Bank 2 | 3 | ![ci-test](https://github.com/samirprakash/go-bank/workflows/ci-test/badge.svg?branch=main) 4 | 5 | ### Features 6 | 7 | - Create and manage account 8 | - Owner 9 | - Balance 10 | - Currency 11 | - Record all balance changes for each account 12 | - Create an account entry for each change for each account 13 | - Money transfer transaction 14 | - Perform money transfer between 2 accounts consistently within a transaction 15 | 16 | ### Pre-requisites 17 | 18 | - Install `docker for desktop` 19 | - Execute `brew install golang-migrate sqlc` 20 | - Execute `go install github.com/golang/mock/mockgen@v1.6.0` 21 | 22 | ### Database Design 23 | 24 | - Design DB schema using dbdiagram.io 25 | - Export the queries onto `/dbdiagrams.io` 26 | - Save and share DB diagram within the team 27 | - Generate SQL code to create database in a target database engine i.e. postgres/MySQL/SQLServer 28 | 29 | ### Docker and Postgres 30 | 31 | - Execute `docker pull postgres:12-alpine` to get the postgres image 32 | - Execute `docker run --name postgres12 -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:12-alpine` to run the postgres container 33 | - Execute `docker logs postgres12` to see the logs 34 | - Execute `docker exec -it postgres12 psql -U root` to connect to the postgres container and login as `root` user 35 | - Connect to postgres container and execute the queries from `/dbdiagrams.io` to create the tables 36 | 37 | ### DB migration 38 | 39 | - Execute `migrate -version` to verify that the `golang-migrate` has been installed 40 | - Execute `migrate create -ext sql -dir db/migration -seq init_schema` to generate migration files 41 | - `*.up.sql` is used to migrate up to a new version using `migrate up` 42 | - `*.down.sql` is used to migrate down to an older version using `migrate down` 43 | - Copy the sql quesries generated from `dbdiagram.io` to `*.up.sql` 44 | - Add `DROP TABLE` queries to `*.down.sql` 45 | - Execute `make migrateup` to migrate data upwards to a new version 46 | - Execute `make migratedown` to revert migration to a previous version 47 | - Manage migrations in future with `migrtion up/down` commands 48 | 49 | ### DB and Docker Setup for development 50 | 51 | - Execute `make postgres` to run postgres container on local docker setup 52 | - Execute `make createdb` to create the `simple_bank` database 53 | - Execute `make migrateup` to setup tables and initial database state 54 | - If required, 55 | - Execute `make dropdb` to drop database 56 | - Execute `make migratedown` to migrate or revert database state to a previous version 57 | 58 | ### Generate CRUD Golang code from SQL 59 | 60 | - Execute `make sqlc` to auto generate CRUD functionalities 61 | - Execute `make mock` to generate mock DB 62 | -------------------------------------------------------------------------------- /db/sqlc/entry_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "testing" 7 | "time" 8 | 9 | "github.com/samirprakash/go-bank/util" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // createRandomEntry creates a random entry to be used in tests 14 | func createRandomEntry(t *testing.T) Entry { 15 | account := createRandomAccount(t) 16 | 17 | arg := CreateEntryParams{ 18 | AccountID: account.ID, 19 | Amount: util.RandomAmount(), 20 | } 21 | 22 | entry, err := testQueries.CreateEntry(context.Background(), arg) 23 | require.NoError(t, err) 24 | require.NotEmpty(t, entry) 25 | 26 | require.Equal(t, arg.AccountID, entry.AccountID) 27 | require.Equal(t, arg.Amount, entry.Amount) 28 | 29 | require.NotZero(t, entry.ID) 30 | require.NotZero(t, entry.CreatedAt) 31 | 32 | return entry 33 | } 34 | 35 | func TestCreateEntry(t *testing.T) { 36 | createRandomEntry(t) 37 | } 38 | 39 | func TestGetEntry(t *testing.T) { 40 | entry1 := createRandomEntry(t) 41 | 42 | entry2, err := testQueries.GetEntry(context.Background(), entry1.ID) 43 | require.NoError(t, err) 44 | require.NotEmpty(t, entry2) 45 | 46 | require.Equal(t, entry1.ID, entry2.ID) 47 | require.Equal(t, entry1.AccountID, entry2.AccountID) 48 | require.Equal(t, entry1.Amount, entry2.Amount) 49 | require.WithinDuration(t, entry1.CreatedAt, entry2.CreatedAt, time.Second) 50 | } 51 | 52 | func TestUpdateEntry(t *testing.T) { 53 | entry1 := createRandomEntry(t) 54 | 55 | arg := UpdateEntryParams{ 56 | ID: entry1.ID, 57 | Amount: util.RandomAmount(), 58 | } 59 | 60 | entry2, err := testQueries.UpdateEntry(context.Background(), arg) 61 | require.NoError(t, err) 62 | require.NotEmpty(t, entry2) 63 | 64 | require.Equal(t, entry1.ID, entry2.ID) 65 | require.Equal(t, entry1.AccountID, entry2.AccountID) 66 | require.Equal(t, arg.Amount, entry2.Amount) 67 | require.WithinDuration(t, entry1.CreatedAt, entry2.CreatedAt, time.Second) 68 | } 69 | 70 | func TestDeleteEntry(t *testing.T) { 71 | entry1 := createRandomEntry(t) 72 | 73 | err := testQueries.DeleteEntry(context.Background(), entry1.ID) 74 | require.NoError(t, err) 75 | 76 | entry2, err := testQueries.GetEntry(context.Background(), entry1.ID) 77 | require.Error(t, err) 78 | require.EqualError(t, err, sql.ErrNoRows.Error()) 79 | require.Empty(t, entry2) 80 | } 81 | 82 | func TestListEntries(t *testing.T) { 83 | for i := 0; i < 10; i++ { 84 | createRandomEntry(t) 85 | } 86 | 87 | arg := ListEntriesParams{ 88 | Limit: 5, 89 | Offset: 5, 90 | } 91 | 92 | entries, err := testQueries.ListEntries(context.Background(), arg) 93 | require.NoError(t, err) 94 | require.Len(t, entries, 5) 95 | 96 | for _, entry := range entries { 97 | require.NotEmpty(t, entry) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /db/sqlc/entry.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.13.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.db.QueryRowContext(ctx, 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 deleteEntry = `-- name: DeleteEntry :exec 39 | DELETE FROM entries 40 | WHERE id = $1 41 | ` 42 | 43 | func (q *Queries) DeleteEntry(ctx context.Context, id int64) error { 44 | _, err := q.db.ExecContext(ctx, deleteEntry, id) 45 | return err 46 | } 47 | 48 | const getEntry = `-- name: GetEntry :one 49 | SELECT id, account_id, amount, created_at FROM entries 50 | WHERE id = $1 LIMIT 1 51 | ` 52 | 53 | func (q *Queries) GetEntry(ctx context.Context, id int64) (Entry, error) { 54 | row := q.db.QueryRowContext(ctx, getEntry, id) 55 | var i Entry 56 | err := row.Scan( 57 | &i.ID, 58 | &i.AccountID, 59 | &i.Amount, 60 | &i.CreatedAt, 61 | ) 62 | return i, err 63 | } 64 | 65 | const listEntries = `-- name: ListEntries :many 66 | SELECT id, account_id, amount, created_at FROM entries 67 | ORDER BY id 68 | LIMIT $1 69 | OFFSET $2 70 | ` 71 | 72 | type ListEntriesParams struct { 73 | Limit int32 `json:"limit"` 74 | Offset int32 `json:"offset"` 75 | } 76 | 77 | func (q *Queries) ListEntries(ctx context.Context, arg ListEntriesParams) ([]Entry, error) { 78 | rows, err := q.db.QueryContext(ctx, listEntries, arg.Limit, arg.Offset) 79 | if err != nil { 80 | return nil, err 81 | } 82 | defer rows.Close() 83 | items := []Entry{} 84 | for rows.Next() { 85 | var i Entry 86 | if err := rows.Scan( 87 | &i.ID, 88 | &i.AccountID, 89 | &i.Amount, 90 | &i.CreatedAt, 91 | ); err != nil { 92 | return nil, err 93 | } 94 | items = append(items, i) 95 | } 96 | if err := rows.Close(); err != nil { 97 | return nil, err 98 | } 99 | if err := rows.Err(); err != nil { 100 | return nil, err 101 | } 102 | return items, nil 103 | } 104 | 105 | const updateEntry = `-- name: UpdateEntry :one 106 | Update entries 107 | SET amount = $2 108 | WHERE id = $1 109 | RETURNING id, account_id, amount, created_at 110 | ` 111 | 112 | type UpdateEntryParams struct { 113 | ID int64 `json:"id"` 114 | Amount int64 `json:"amount"` 115 | } 116 | 117 | func (q *Queries) UpdateEntry(ctx context.Context, arg UpdateEntryParams) (Entry, error) { 118 | row := q.db.QueryRowContext(ctx, updateEntry, arg.ID, arg.Amount) 119 | var i Entry 120 | err := row.Scan( 121 | &i.ID, 122 | &i.AccountID, 123 | &i.Amount, 124 | &i.CreatedAt, 125 | ) 126 | return i, err 127 | } 128 | -------------------------------------------------------------------------------- /db/sqlc/account_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "testing" 7 | "time" 8 | 9 | "github.com/samirprakash/go-bank/util" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // createRandomAccount creates a random account to be used in tests 14 | func createRandomAccount(t *testing.T) Account { 15 | user := createRandomUser(t) 16 | 17 | arg := CreateAccountParams{ 18 | Owner: user.Username, 19 | Balance: util.RandomAmount(), 20 | Currency: util.RandomCurrency(), 21 | } 22 | 23 | account, err := testQueries.CreateAccount(context.Background(), arg) 24 | require.NoError(t, err) 25 | require.NotEmpty(t, account) 26 | 27 | require.Equal(t, arg.Owner, account.Owner) 28 | require.Equal(t, arg.Balance, account.Balance) 29 | require.Equal(t, arg.Currency, account.Currency) 30 | 31 | require.NotZero(t, account.ID) 32 | require.NotZero(t, account.CreatedAt) 33 | 34 | return account 35 | } 36 | 37 | func TestCreateAccount(t *testing.T) { 38 | createRandomAccount(t) 39 | } 40 | 41 | func TestGetAccount(t *testing.T) { 42 | account1 := createRandomAccount(t) 43 | 44 | account2, err := testQueries.GetAccount(context.Background(), account1.ID) 45 | require.NoError(t, err) 46 | require.NotEmpty(t, account2) 47 | 48 | require.Equal(t, account1.ID, account2.ID) 49 | require.Equal(t, account1.Owner, account2.Owner) 50 | require.Equal(t, account1.Balance, account2.Balance) 51 | require.Equal(t, account1.Currency, account2.Currency) 52 | require.WithinDuration(t, account1.CreatedAt, account2.CreatedAt, time.Second) 53 | } 54 | 55 | func TestUpdateAccount(t *testing.T) { 56 | account1 := createRandomAccount(t) 57 | 58 | arg := UpdateAccountParams{ 59 | ID: account1.ID, 60 | Balance: util.RandomAmount(), 61 | } 62 | 63 | account2, err := testQueries.UpdateAccount(context.Background(), arg) 64 | require.NoError(t, err) 65 | require.NotEmpty(t, account2) 66 | 67 | require.Equal(t, account1.ID, account2.ID) 68 | require.Equal(t, account1.Owner, account2.Owner) 69 | require.Equal(t, arg.Balance, account2.Balance) 70 | require.Equal(t, account1.Currency, account2.Currency) 71 | require.WithinDuration(t, account1.CreatedAt, account2.CreatedAt, time.Second) 72 | } 73 | 74 | func TestDeleteAccount(t *testing.T) { 75 | account1 := createRandomAccount(t) 76 | 77 | err := testQueries.DeleteAccount(context.Background(), account1.ID) 78 | require.NoError(t, err) 79 | 80 | account2, err := testQueries.GetAccount(context.Background(), account1.ID) 81 | require.Error(t, err) 82 | require.EqualError(t, err, sql.ErrNoRows.Error()) 83 | require.Empty(t, account2) 84 | } 85 | 86 | func TestListAccounts(t *testing.T) { 87 | var lastAccount Account 88 | 89 | for i := 0; i < 10; i++ { 90 | lastAccount = createRandomAccount(t) 91 | } 92 | 93 | arg := ListAccountsParams{ 94 | Owner: lastAccount.Owner, 95 | Limit: 5, 96 | Offset: 0, 97 | } 98 | 99 | accounts, err := testQueries.ListAccounts(context.Background(), arg) 100 | require.NoError(t, err) 101 | require.NotEmpty(t, accounts) 102 | 103 | for _, account := range accounts { 104 | require.NotEmpty(t, account) 105 | require.Equal(t, lastAccount.Owner, account.Owner) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /db/sqlc/transfer_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "testing" 7 | "time" 8 | 9 | "github.com/samirprakash/go-bank/util" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // createRandomTransfer creates a random transfer to be used in the tests 14 | func createRandomTransfer(t *testing.T) Transfer { 15 | account1 := createRandomAccount(t) 16 | account2 := createRandomAccount(t) 17 | 18 | arg := CreateTransferParams{ 19 | FromAccountID: account1.ID, 20 | ToAccountID: account2.ID, 21 | Amount: util.RandomAmount(), 22 | } 23 | 24 | transfer, err := testQueries.CreateTransfer(context.Background(), arg) 25 | require.NoError(t, err) 26 | require.NotEmpty(t, transfer) 27 | 28 | require.Equal(t, account1.ID, transfer.FromAccountID) 29 | require.Equal(t, account2.ID, transfer.ToAccountID) 30 | require.Equal(t, arg.Amount, transfer.Amount) 31 | require.WithinDuration(t, account2.CreatedAt, transfer.CreatedAt, time.Second) 32 | 33 | return transfer 34 | } 35 | 36 | func TestCreateTransfer(t *testing.T) { 37 | createRandomTransfer(t) 38 | } 39 | 40 | func TestGetTransfer(t *testing.T) { 41 | transfer1 := createRandomTransfer(t) 42 | 43 | transfer2, err := testQueries.GetTransfer(context.Background(), transfer1.ID) 44 | require.NoError(t, err) 45 | require.NotEmpty(t, transfer2) 46 | 47 | require.Equal(t, transfer1.ID, transfer2.ID) 48 | require.Equal(t, transfer1.FromAccountID, transfer2.FromAccountID) 49 | require.Equal(t, transfer1.ToAccountID, transfer2.ToAccountID) 50 | require.Equal(t, transfer1.Amount, transfer2.Amount) 51 | require.WithinDuration(t, transfer1.CreatedAt, transfer2.CreatedAt, time.Second) 52 | } 53 | 54 | func TestUpdateTransfer(t *testing.T) { 55 | transfer1 := createRandomTransfer(t) 56 | 57 | arg := UpdateTransferParams{ 58 | ID: transfer1.ID, 59 | Amount: util.RandomAmount(), 60 | } 61 | 62 | transfer2, err := testQueries.UpdateTransfer(context.Background(), arg) 63 | require.NoError(t, err) 64 | require.NotEmpty(t, transfer2) 65 | 66 | require.Equal(t, transfer1.ID, transfer2.ID) 67 | require.Equal(t, transfer1.FromAccountID, transfer2.FromAccountID) 68 | require.Equal(t, transfer1.ToAccountID, transfer2.ToAccountID) 69 | require.Equal(t, arg.Amount, transfer2.Amount) 70 | require.WithinDuration(t, transfer1.CreatedAt, transfer2.CreatedAt, time.Second) 71 | } 72 | 73 | func TestDeleteTransfer(t *testing.T) { 74 | transfer1 := createRandomTransfer(t) 75 | 76 | err := testQueries.DeleteTransfer(context.Background(), transfer1.ID) 77 | require.NoError(t, err) 78 | 79 | transfer2, err := testQueries.GetTransfer(context.Background(), transfer1.ID) 80 | require.Error(t, err) 81 | require.EqualError(t, err, sql.ErrNoRows.Error()) 82 | require.Empty(t, transfer2) 83 | } 84 | 85 | func TestListTransfers(t *testing.T) { 86 | for i := 0; i < 10; i++ { 87 | createRandomTransfer(t) 88 | } 89 | 90 | arg := ListTransfersParams{ 91 | Limit: 5, 92 | Offset: 5, 93 | } 94 | 95 | transfers, err := testQueries.ListTransfers(context.Background(), arg) 96 | require.NoError(t, err) 97 | require.Len(t, transfers, 5) 98 | 99 | for _, transfer := range transfers { 100 | require.NotEmpty(t, transfer) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /db/sqlc/transfer.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.13.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.db.QueryRowContext(ctx, 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 deleteTransfer = `-- name: DeleteTransfer :exec 42 | DELETE FROM transfers 43 | WHERE id = $1 44 | ` 45 | 46 | func (q *Queries) DeleteTransfer(ctx context.Context, id int64) error { 47 | _, err := q.db.ExecContext(ctx, deleteTransfer, id) 48 | return err 49 | } 50 | 51 | const getTransfer = `-- name: GetTransfer :one 52 | SELECT id, from_account_id, to_account_id, amount, created_at FROM transfers 53 | WHERE id = $1 LIMIT 1 54 | ` 55 | 56 | func (q *Queries) GetTransfer(ctx context.Context, id int64) (Transfer, error) { 57 | row := q.db.QueryRowContext(ctx, getTransfer, id) 58 | var i Transfer 59 | err := row.Scan( 60 | &i.ID, 61 | &i.FromAccountID, 62 | &i.ToAccountID, 63 | &i.Amount, 64 | &i.CreatedAt, 65 | ) 66 | return i, err 67 | } 68 | 69 | const listTransfers = `-- name: ListTransfers :many 70 | SELECT id, from_account_id, to_account_id, amount, created_at FROM transfers 71 | ORDER BY id 72 | LIMIT $1 73 | OFFSET $2 74 | ` 75 | 76 | type ListTransfersParams struct { 77 | Limit int32 `json:"limit"` 78 | Offset int32 `json:"offset"` 79 | } 80 | 81 | func (q *Queries) ListTransfers(ctx context.Context, arg ListTransfersParams) ([]Transfer, error) { 82 | rows, err := q.db.QueryContext(ctx, listTransfers, arg.Limit, arg.Offset) 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 | 110 | const updateTransfer = `-- name: UpdateTransfer :one 111 | Update transfers 112 | SET amount = $2 113 | WHERE id = $1 114 | RETURNING id, from_account_id, to_account_id, amount, created_at 115 | ` 116 | 117 | type UpdateTransferParams struct { 118 | ID int64 `json:"id"` 119 | Amount int64 `json:"amount"` 120 | } 121 | 122 | func (q *Queries) UpdateTransfer(ctx context.Context, arg UpdateTransferParams) (Transfer, error) { 123 | row := q.db.QueryRowContext(ctx, updateTransfer, arg.ID, arg.Amount) 124 | var i Transfer 125 | err := row.Scan( 126 | &i.ID, 127 | &i.FromAccountID, 128 | &i.ToAccountID, 129 | &i.Amount, 130 | &i.CreatedAt, 131 | ) 132 | return i, err 133 | } 134 | -------------------------------------------------------------------------------- /api/middleware_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/samirprakash/go-bank/token" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func addAuthorization(t *testing.T, request *http.Request, tokenMaker token.Maker, authorizationType string, username string, duration time.Duration) { 16 | token, payload, err := tokenMaker.CreateToken(username, duration) 17 | require.NoError(t, err) 18 | require.NotEmpty(t, token) 19 | require.NotEmpty(t, payload) 20 | 21 | authorizationHeader := fmt.Sprintf("%s %s", authorizationType, token) 22 | request.Header.Set(authorizationHeaderKey, authorizationHeader) 23 | } 24 | 25 | func TestAuthMiddleware(t *testing.T) { 26 | testCases := []struct { 27 | name string 28 | setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker) 29 | checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder) 30 | }{ 31 | { 32 | name: "OK", 33 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 34 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", time.Minute) 35 | }, 36 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 37 | require.Equal(t, http.StatusOK, recorder.Code) 38 | }, 39 | }, 40 | { 41 | name: "NoAuthorization", 42 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 43 | }, 44 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 45 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 46 | }, 47 | }, 48 | { 49 | name: "UnsupportedAuthorization", 50 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 51 | addAuthorization(t, request, tokenMaker, "unsupported", "user", time.Minute) 52 | }, 53 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 54 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 55 | }, 56 | }, 57 | { 58 | name: "InvalidAuthrizationFormat", 59 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 60 | addAuthorization(t, request, tokenMaker, "", "user", time.Minute) 61 | }, 62 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 63 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 64 | }, 65 | }, 66 | { 67 | name: "ExpiredAuthorization", 68 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 69 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", -time.Minute) 70 | }, 71 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 72 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 73 | }, 74 | }, 75 | } 76 | 77 | for i := range testCases { 78 | tc := testCases[i] 79 | 80 | t.Run(tc.name, func(t *testing.T) { 81 | server := newTestServer(t, nil) 82 | 83 | authPath := "/auth" 84 | server.router.GET( 85 | authPath, 86 | authMiddleware(server.tokenMaker), 87 | func(ctx *gin.Context) { 88 | ctx.JSON(http.StatusOK, gin.H{}) 89 | }, 90 | ) 91 | 92 | recorder := httptest.NewRecorder() 93 | request, err := http.NewRequest(http.MethodGet, authPath, nil) 94 | require.NoError(t, err) 95 | 96 | tc.setupAuth(t, request, server.tokenMaker) 97 | server.router.ServeHTTP(recorder, request) 98 | tc.checkResponse(t, recorder) 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /db/sqlc/account.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.13.0 4 | // source: account.sql 5 | 6 | package db 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const createAccount = `-- name: CreateAccount :one 13 | INSERT INTO accounts ( 14 | owner, 15 | balance, 16 | currency 17 | ) VALUES ( 18 | $1, $2, $3 19 | ) RETURNING id, owner, balance, currency, created_at 20 | ` 21 | 22 | type CreateAccountParams struct { 23 | Owner string `json:"owner"` 24 | Balance int64 `json:"balance"` 25 | Currency string `json:"currency"` 26 | } 27 | 28 | func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) { 29 | row := q.db.QueryRowContext(ctx, createAccount, arg.Owner, arg.Balance, arg.Currency) 30 | var i Account 31 | err := row.Scan( 32 | &i.ID, 33 | &i.Owner, 34 | &i.Balance, 35 | &i.Currency, 36 | &i.CreatedAt, 37 | ) 38 | return i, err 39 | } 40 | 41 | const deleteAccount = `-- name: DeleteAccount :exec 42 | DELETE FROM accounts 43 | WHERE id = $1 44 | ` 45 | 46 | func (q *Queries) DeleteAccount(ctx context.Context, id int64) error { 47 | _, err := q.db.ExecContext(ctx, deleteAccount, id) 48 | return err 49 | } 50 | 51 | const getAccount = `-- name: GetAccount :one 52 | SELECT id, owner, balance, currency, created_at FROM accounts 53 | WHERE id = $1 LIMIT 1 54 | ` 55 | 56 | func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) { 57 | row := q.db.QueryRowContext(ctx, getAccount, id) 58 | var i Account 59 | err := row.Scan( 60 | &i.ID, 61 | &i.Owner, 62 | &i.Balance, 63 | &i.Currency, 64 | &i.CreatedAt, 65 | ) 66 | return i, err 67 | } 68 | 69 | const getAccountForUpdate = `-- name: GetAccountForUpdate :one 70 | SELECT id, owner, balance, currency, created_at FROM accounts 71 | WHERE id = $1 LIMIT 1 72 | FOR NO KEY UPDATE 73 | ` 74 | 75 | func (q *Queries) GetAccountForUpdate(ctx context.Context, id int64) (Account, error) { 76 | row := q.db.QueryRowContext(ctx, getAccountForUpdate, id) 77 | var i Account 78 | err := row.Scan( 79 | &i.ID, 80 | &i.Owner, 81 | &i.Balance, 82 | &i.Currency, 83 | &i.CreatedAt, 84 | ) 85 | return i, err 86 | } 87 | 88 | const listAccounts = `-- name: ListAccounts :many 89 | SELECT id, owner, balance, currency, created_at FROM accounts 90 | WHERE OWNER = $1 91 | ORDER BY id 92 | LIMIT $2 93 | OFFSET $3 94 | ` 95 | 96 | type ListAccountsParams struct { 97 | Owner string `json:"owner"` 98 | Limit int32 `json:"limit"` 99 | Offset int32 `json:"offset"` 100 | } 101 | 102 | func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) { 103 | rows, err := q.db.QueryContext(ctx, listAccounts, arg.Owner, arg.Limit, arg.Offset) 104 | if err != nil { 105 | return nil, err 106 | } 107 | defer rows.Close() 108 | items := []Account{} 109 | for rows.Next() { 110 | var i Account 111 | if err := rows.Scan( 112 | &i.ID, 113 | &i.Owner, 114 | &i.Balance, 115 | &i.Currency, 116 | &i.CreatedAt, 117 | ); err != nil { 118 | return nil, err 119 | } 120 | items = append(items, i) 121 | } 122 | if err := rows.Close(); err != nil { 123 | return nil, err 124 | } 125 | if err := rows.Err(); err != nil { 126 | return nil, err 127 | } 128 | return items, nil 129 | } 130 | 131 | const updateAccount = `-- name: UpdateAccount :one 132 | Update accounts 133 | SET balance = $2 134 | WHERE id = $1 135 | RETURNING id, owner, balance, currency, created_at 136 | ` 137 | 138 | type UpdateAccountParams struct { 139 | ID int64 `json:"id"` 140 | Balance int64 `json:"balance"` 141 | } 142 | 143 | func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) { 144 | row := q.db.QueryRowContext(ctx, updateAccount, arg.ID, arg.Balance) 145 | var i Account 146 | err := row.Scan( 147 | &i.ID, 148 | &i.Owner, 149 | &i.Balance, 150 | &i.Currency, 151 | &i.CreatedAt, 152 | ) 153 | return i, err 154 | } 155 | 156 | const updateAccountBalance = `-- name: UpdateAccountBalance :one 157 | Update accounts 158 | SET balance = balance + $1 159 | WHERE id = $2 160 | RETURNING id, owner, balance, currency, created_at 161 | ` 162 | 163 | type UpdateAccountBalanceParams struct { 164 | Amount int64 `json:"amount"` 165 | ID int64 `json:"id"` 166 | } 167 | 168 | func (q *Queries) UpdateAccountBalance(ctx context.Context, arg UpdateAccountBalanceParams) (Account, error) { 169 | row := q.db.QueryRowContext(ctx, updateAccountBalance, arg.Amount, arg.ID) 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 | -------------------------------------------------------------------------------- /api/account_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | "time" 13 | 14 | "github.com/golang/mock/gomock" 15 | mockdb "github.com/samirprakash/go-bank/db/mock" 16 | db "github.com/samirprakash/go-bank/db/sqlc" 17 | "github.com/samirprakash/go-bank/token" 18 | "github.com/samirprakash/go-bank/util" 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 | requireBodyMatchAccount(t, recorder.Body, account) 45 | }, 46 | }, 47 | { 48 | name: "NotFound", 49 | accountID: account.ID, 50 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 51 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) 52 | }, 53 | buildStubs: func(store *mockdb.MockStore) { 54 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(db.Account{}, sql.ErrNoRows) 55 | }, 56 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 57 | require.Equal(t, http.StatusNotFound, recorder.Code) 58 | }, 59 | }, 60 | { 61 | name: "InternalError", 62 | accountID: account.ID, 63 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 64 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) 65 | }, 66 | buildStubs: func(store *mockdb.MockStore) { 67 | store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(db.Account{}, sql.ErrConnDone) 68 | }, 69 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 70 | require.Equal(t, http.StatusInternalServerError, recorder.Code) 71 | }, 72 | }, 73 | { 74 | name: "InvalidID", 75 | accountID: 0, 76 | setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { 77 | addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) 78 | }, 79 | buildStubs: func(store *mockdb.MockStore) { 80 | store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) 81 | }, 82 | checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { 83 | require.Equal(t, http.StatusBadRequest, recorder.Code) 84 | }, 85 | }, 86 | } 87 | 88 | for i := range testCases { 89 | tc := testCases[i] 90 | 91 | t.Run(tc.name, func(t *testing.T) { 92 | ctrl := gomock.NewController(t) 93 | defer ctrl.Finish() 94 | 95 | store := mockdb.NewMockStore(ctrl) 96 | // build stubs 97 | tc.buildStubs(store) 98 | 99 | // start test server and send request 100 | server := newTestServer(t, store) 101 | recorder := httptest.NewRecorder() 102 | 103 | url := fmt.Sprintf("/accounts/%d", tc.accountID) 104 | request, err := http.NewRequest(http.MethodGet, url, nil) 105 | require.NoError(t, err) 106 | 107 | tc.setupAuth(t, request, server.tokenMaker) 108 | 109 | // server with recorder and request 110 | server.router.ServeHTTP(recorder, request) 111 | // check response 112 | tc.checkResponse(t, recorder) 113 | }) 114 | } 115 | } 116 | 117 | // randomAccount generates a random account for API testing 118 | func randomAccount(owner string) db.Account { 119 | return db.Account{ 120 | ID: util.RandomInt(1, 1000), 121 | Owner: owner, 122 | Balance: util.RandomAmount(), 123 | Currency: util.RandomCurrency(), 124 | } 125 | } 126 | 127 | // requireBodyMatchAccount matches the response account from a GetAccount API request 128 | // with the random account that was created for testing 129 | func requireBodyMatchAccount(t *testing.T, body *bytes.Buffer, account db.Account) { 130 | data, err := ioutil.ReadAll(body) 131 | require.NoError(t, err) 132 | 133 | var gotAccount db.Account 134 | err = json.Unmarshal(data, &gotAccount) 135 | require.NoError(t, err) 136 | require.Equal(t, account, gotAccount) 137 | } 138 | -------------------------------------------------------------------------------- /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 | set -- "$@" -- "$TIMEOUT" "$QUIET" "$PROTOCOL" "$HOST" "$PORT" "$result" 26 | TIMEOUT=15 27 | QUIET=0 28 | # The protocol to make the request with, either "tcp" or "http" 29 | PROTOCOL="tcp" 30 | 31 | echoerr() { 32 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi 33 | } 34 | 35 | usage() { 36 | exitcode="$1" 37 | cat << USAGE >&2 38 | Usage: 39 | $0 host:port|url [-t timeout] [-- command args] 40 | -q | --quiet Do not output any status messages 41 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout 42 | -- COMMAND ARGS Execute command with args after the test finishes 43 | USAGE 44 | exit "$exitcode" 45 | } 46 | 47 | wait_for() { 48 | case "$PROTOCOL" in 49 | tcp) 50 | if ! command -v nc >/dev/null; then 51 | echoerr 'nc command is missing!' 52 | exit 1 53 | fi 54 | ;; 55 | wget) 56 | if ! command -v wget >/dev/null; then 57 | echoerr 'nc command is missing!' 58 | exit 1 59 | fi 60 | ;; 61 | esac 62 | 63 | while :; do 64 | case "$PROTOCOL" in 65 | tcp) 66 | nc -w 1 -z "$HOST" "$PORT" > /dev/null 2>&1 67 | ;; 68 | http) 69 | wget --timeout=1 -q "$HOST" -O /dev/null > /dev/null 2>&1 70 | ;; 71 | *) 72 | echoerr "Unknown protocol '$PROTOCOL'" 73 | exit 1 74 | ;; 75 | esac 76 | 77 | result=$? 78 | 79 | if [ $result -eq 0 ] ; then 80 | if [ $# -gt 7 ] ; then 81 | for result in $(seq $(($# - 7))); do 82 | result=$1 83 | shift 84 | set -- "$@" "$result" 85 | done 86 | 87 | TIMEOUT=$2 QUIET=$3 PROTOCOL=$4 HOST=$5 PORT=$6 result=$7 88 | shift 7 89 | exec "$@" 90 | fi 91 | exit 0 92 | fi 93 | 94 | if [ "$TIMEOUT" -le 0 ]; then 95 | break 96 | fi 97 | TIMEOUT=$((TIMEOUT - 1)) 98 | 99 | sleep 1 100 | done 101 | echo "Operation timed out" >&2 102 | exit 1 103 | } 104 | 105 | while :; do 106 | case "$1" in 107 | http://*|https://*) 108 | HOST="$1" 109 | PROTOCOL="http" 110 | shift 1 111 | ;; 112 | *:* ) 113 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1) 114 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2) 115 | shift 1 116 | ;; 117 | -q | --quiet) 118 | QUIET=1 119 | shift 1 120 | ;; 121 | -q-*) 122 | QUIET=0 123 | echoerr "Unknown option: $1" 124 | usage 1 125 | ;; 126 | -q*) 127 | QUIET=1 128 | result=$1 129 | shift 1 130 | set -- -"${result#-q}" "$@" 131 | ;; 132 | -t | --timeout) 133 | TIMEOUT="$2" 134 | shift 2 135 | ;; 136 | -t*) 137 | TIMEOUT="${1#-t}" 138 | shift 1 139 | ;; 140 | --timeout=*) 141 | TIMEOUT="${1#*=}" 142 | shift 1 143 | ;; 144 | --) 145 | shift 146 | break 147 | ;; 148 | --help) 149 | usage 0 150 | ;; 151 | -*) 152 | QUIET=0 153 | echoerr "Unknown option: $1" 154 | usage 1 155 | ;; 156 | *) 157 | QUIET=0 158 | echoerr "Unknown argument: $1" 159 | usage 1 160 | ;; 161 | esac 162 | done 163 | 164 | if ! [ "$TIMEOUT" -ge 0 ] 2>/dev/null; then 165 | echoerr "Error: invalid timeout '$TIMEOUT'" 166 | usage 3 167 | fi 168 | 169 | case "$PROTOCOL" in 170 | tcp) 171 | if [ "$HOST" = "" ] || [ "$PORT" = "" ]; then 172 | echoerr "Error: you need to provide a host and port to test." 173 | usage 2 174 | fi 175 | ;; 176 | http) 177 | if [ "$HOST" = "" ]; then 178 | echoerr "Error: you need to provide a host to test." 179 | usage 2 180 | fi 181 | ;; 182 | esac 183 | 184 | wait_for "$@" 185 | -------------------------------------------------------------------------------- /api/user.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "database/sql" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/google/uuid" 10 | "github.com/lib/pq" 11 | db "github.com/samirprakash/go-bank/db/sqlc" 12 | "github.com/samirprakash/go-bank/util" 13 | ) 14 | 15 | type createUserRequest struct { 16 | Username string `json:"username" binding:"required,alphanum"` 17 | Password string `json:"password" binding:"required,min=6"` 18 | FullName string `json:"full_name" binding:"required"` 19 | Email string `json:"email" binding:"required,email"` 20 | } 21 | 22 | type userResponse struct { 23 | Username string `json:"username"` 24 | FullName string `json:"full_name"` 25 | Email string `json:"email"` 26 | PasswordChangedAt time.Time `json:"password_changed_at"` 27 | CreatedAt time.Time `json:"created_at"` 28 | } 29 | 30 | func newUserResponse(user db.User) userResponse { 31 | return userResponse{ 32 | Username: user.Username, 33 | FullName: user.FullName, 34 | Email: user.Email, 35 | PasswordChangedAt: user.PasswordChangedAt, 36 | CreatedAt: user.CreatedAt, 37 | } 38 | } 39 | 40 | func (server *Server) createUser(ctx *gin.Context) { 41 | var req createUserRequest 42 | 43 | // validate request params 44 | if err := ctx.ShouldBindJSON(&req); err != nil { 45 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 46 | return 47 | } 48 | 49 | // create hashed password 50 | hashedPassword, err := util.HashPassword(req.Password) 51 | if err != nil { 52 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 53 | return 54 | } 55 | 56 | // create db params 57 | arg := db.CreateUserParams{ 58 | Username: req.Username, 59 | HashedPassword: hashedPassword, 60 | FullName: req.FullName, 61 | Email: req.Email, 62 | } 63 | 64 | // save to db 65 | user, err := server.store.CreateUser(ctx, arg) 66 | if err != nil { 67 | if pqErr, ok := err.(*pq.Error); ok { 68 | switch pqErr.Code.Name() { 69 | case "unique_violation": 70 | ctx.JSON(http.StatusForbidden, errorResponse(err)) 71 | return 72 | } 73 | } 74 | 75 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 76 | return 77 | } 78 | 79 | // return response 80 | res := newUserResponse(user) 81 | 82 | ctx.JSON(http.StatusOK, res) 83 | } 84 | 85 | type loginUserRequest struct { 86 | Username string `json:"username" binding:"required,alphanum"` 87 | Password string `json:"password" binding:"required,min=6"` 88 | } 89 | 90 | type loginUserResponse struct { 91 | SessionID uuid.UUID `json:"session_id"` 92 | AccessToken string `json:"access_token"` 93 | AccessTokenExpiresAt time.Time `json:"access_token_expires_at"` 94 | RefreshToken string `json:"refresh_token"` 95 | RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"` 96 | User userResponse `json:"user"` 97 | } 98 | 99 | func (server *Server) loginUser(ctx *gin.Context) { 100 | var req loginUserRequest 101 | 102 | if err := ctx.ShouldBindJSON(&req); err != nil { 103 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 104 | return 105 | } 106 | 107 | user, err := server.store.GetUser(ctx, req.Username) 108 | if err != nil { 109 | if err == sql.ErrNoRows { 110 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 111 | return 112 | } 113 | 114 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 115 | return 116 | } 117 | 118 | err = util.CheckPassword(req.Password, user.HashedPassword) 119 | if err != nil { 120 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 121 | return 122 | } 123 | 124 | accessToken, accessPayload, err := server.tokenMaker.CreateToken(user.Username, server.config.AccessTokenDuration) 125 | if err != nil { 126 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 127 | return 128 | } 129 | 130 | refreshToken, refreshPayload, err := server.tokenMaker.CreateToken(user.Username, server.config.RefreshTokenDuration) 131 | if err != nil { 132 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 133 | return 134 | } 135 | 136 | session, err := server.store.CreateSession(ctx, db.CreateSessionParams{ 137 | ID: refreshPayload.ID, 138 | Username: user.Username, 139 | RefreshToken: refreshToken, 140 | UserAgent: ctx.Request.UserAgent(), 141 | ClientIp: ctx.ClientIP(), 142 | IsBlocked: false, 143 | ExpiresAt: refreshPayload.ExpiredAt, 144 | }) 145 | 146 | if err != nil { 147 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 148 | return 149 | } 150 | 151 | rsp := loginUserResponse{ 152 | SessionID: session.ID, 153 | AccessToken: accessToken, 154 | AccessTokenExpiresAt: accessPayload.ExpiredAt, 155 | RefreshToken: refreshToken, 156 | RefreshTokenExpiresAt: refreshPayload.ExpiredAt, 157 | User: newUserResponse(user), 158 | } 159 | 160 | ctx.JSON(http.StatusOK, rsp) 161 | } 162 | -------------------------------------------------------------------------------- /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 transactions. 10 | // We need to extend on the exisiting *Queries struct that sqlc provides as it only supports executing queries on one table at a time. 11 | // In order to execute transactions, we will use store to create a set of quesries to be executed in sequence 12 | type Store interface { 13 | Querier 14 | TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) 15 | } 16 | 17 | // SQLStore provides all functions to execute SQL queries and transactions. 18 | // We need to extend on the exisiting *Queries struct that sqlc provides as it only supports executing queries on one table at a time. 19 | // In order to execute transactions, we will use store to create a set of quesries to be executed in sequence 20 | type SQLStore struct { 21 | db *sql.DB 22 | *Queries 23 | } 24 | 25 | // NewStore creates a new store 26 | func NewStore(db *sql.DB) Store { 27 | return &SQLStore{ 28 | Queries: New(db), 29 | db: db, 30 | } 31 | } 32 | 33 | // execTx executes a function within a database transaction. 34 | // This function is added on to the store and it takes context and a callback function as parameters. 35 | // When we start a new transaction, we will create a set of queries and then call the callback function with those queries to execute the transaction. 36 | // If there is a problem, then there is an option to rollback or else the transaction can be committed. 37 | func (store *SQLStore) execTx(ctx context.Context, fn func(*Queries) error) error { 38 | tx, err := store.db.BeginTx(ctx, nil) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | q := New(tx) 44 | err = fn(q) 45 | if err != nil { 46 | // rollback if there is an error 47 | if rbErr := tx.Rollback(); rbErr != nil { 48 | return fmt.Errorf("transaction error : %v, rollback error: %v", err, rbErr) 49 | } 50 | return err 51 | } 52 | // commit if everything works 53 | return tx.Commit() 54 | } 55 | 56 | // TransferTxParams represents the arguments required to execute the transfer transaction 57 | type TransferTxParams struct { 58 | FromAccountID int64 `json:"from_account_id,omitempty"` 59 | ToAccountID int64 `json:"to_account_id,omitempty"` 60 | Amount int64 `json:"amount,omitempty"` 61 | } 62 | 63 | // TransferTxResult represents the result of the transfer transaction 64 | type TransferTxResult struct { 65 | Transfer Transfer `json:"transfer,omitempty"` 66 | FromAccount Account `json:"from_account,omitempty"` 67 | ToAccount Account `json:"to_account,omitempty"` 68 | FromEntry Entry `json:"from_entry,omitempty"` 69 | ToEntry Entry `json:"to_entry,omitempty"` 70 | } 71 | 72 | // TransferTx performs a money transfer from one account to another. 73 | // It executes a database transaction to create a transfer record, update account entries and update the account balance. 74 | func (store *SQLStore) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) { 75 | var result TransferTxResult 76 | 77 | err := store.execTx(ctx, func(q *Queries) error { 78 | var err error 79 | 80 | // create a transfer query and execute it 81 | result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{ 82 | FromAccountID: arg.FromAccountID, 83 | ToAccountID: arg.ToAccountID, 84 | Amount: arg.Amount, 85 | }) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | // create an entry for the account from which money has been transferred 91 | result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{ 92 | AccountID: arg.FromAccountID, 93 | Amount: -arg.Amount, 94 | }) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | // create an entry for the acoount to which the money has been transferred 100 | result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{ 101 | AccountID: arg.ToAccountID, 102 | Amount: arg.Amount, 103 | }) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | //Update account balance 109 | if arg.FromAccountID < arg.ToAccountID { 110 | result.FromAccount, result.ToAccount, err = updateBalancesToAccounts(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount) 111 | } else { 112 | result.ToAccount, result.FromAccount, err = updateBalancesToAccounts(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount) 113 | } 114 | return nil 115 | }) 116 | 117 | return result, err 118 | } 119 | 120 | func updateBalancesToAccounts(ctx context.Context, q *Queries, accountID1 int64, amount1 int64, accountID2 int64, amount2 int64) (account1 Account, account2 Account, err error) { 121 | account1, err = q.UpdateAccountBalance(ctx, UpdateAccountBalanceParams{ 122 | Amount: amount1, 123 | ID: accountID1, 124 | }) 125 | if err != nil { 126 | return 127 | } 128 | 129 | account2, err = q.UpdateAccountBalance(ctx, UpdateAccountBalanceParams{ 130 | Amount: amount2, 131 | ID: accountID2, 132 | }) 133 | if err != nil { 134 | return 135 | } 136 | 137 | return 138 | } 139 | -------------------------------------------------------------------------------- /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 | // channels for receiving errors and results from transaction gorutines 23 | errs := make(chan error) 24 | results := make(chan TransferTxResult) 25 | 26 | // execute transactions in goroutines and 27 | // send errors and results back to the main gorutine for validations 28 | for i := 0; i < n; i++ { 29 | go func() { 30 | result, err := store.TransferTx(context.Background(), TransferTxParams{ 31 | FromAccountID: account1.ID, 32 | ToAccountID: account2.ID, 33 | Amount: amount, 34 | }) 35 | 36 | errs <- err 37 | results <- result 38 | }() 39 | } 40 | 41 | // check results 42 | existed := make(map[int]bool) 43 | 44 | for i := 0; i < n; i++ { 45 | err := <-errs 46 | require.NoError(t, err) 47 | 48 | result := <-results 49 | require.NotEmpty(t, result) 50 | 51 | // check transfer from extracted result 52 | transfer := result.Transfer 53 | require.NotEmpty(t, transfer) 54 | require.Equal(t, account1.ID, transfer.FromAccountID) 55 | require.Equal(t, account2.ID, transfer.ToAccountID) 56 | require.Equal(t, amount, transfer.Amount) 57 | require.NotZero(t, transfer.ID) 58 | require.NotZero(t, transfer.CreatedAt) 59 | 60 | // check that transfer record is being created in the database 61 | _, err = store.GetTransfer(context.Background(), transfer.ID) 62 | require.NoError(t, err) 63 | 64 | // check fromEntry 65 | fromEntry := result.FromEntry 66 | require.NotEmpty(t, fromEntry) 67 | require.Equal(t, account1.ID, fromEntry.AccountID) 68 | require.Equal(t, -amount, fromEntry.Amount) 69 | require.NotZero(t, fromEntry.ID) 70 | require.NotEmpty(t, fromEntry.CreatedAt) 71 | 72 | // check that entry record has been created in the database 73 | _, err = store.GetEntry(context.Background(), fromEntry.ID) 74 | require.NoError(t, err) 75 | 76 | // check toEntry 77 | toEntry := result.ToEntry 78 | require.NotEmpty(t, toEntry) 79 | require.Equal(t, account2.ID, toEntry.AccountID) 80 | require.Equal(t, amount, toEntry.Amount) 81 | require.NotZero(t, toEntry.ID) 82 | require.NotEmpty(t, toEntry.CreatedAt) 83 | 84 | // check that entry record has been created in the database 85 | _, err = store.GetEntry(context.Background(), toEntry.ID) 86 | require.NoError(t, err) 87 | 88 | // check accounts 89 | fromAccount := result.FromAccount 90 | require.NotEmpty(t, fromAccount) 91 | require.Equal(t, account1.ID, fromAccount.ID) 92 | 93 | toAccount := result.ToAccount 94 | require.NotEmpty(t, toAccount) 95 | require.Equal(t, account2.ID, toAccount.ID) 96 | 97 | // check account balance 98 | fmt.Println(">> after :::", account1.Balance, account2.Balance) 99 | 100 | diff1 := account1.Balance - fromAccount.Balance 101 | diff2 := toAccount.Balance - account2.Balance 102 | require.Equal(t, diff1, diff2) 103 | require.True(t, diff1 > 0) 104 | require.True(t, diff1%amount == 0) 105 | 106 | k := int(diff1 / amount) 107 | require.True(t, k >= 1 && k <= n) 108 | require.NotContains(t, existed, k) 109 | require.NotContains(t, existed, k) 110 | existed[k] = true 111 | } 112 | 113 | // check final updated account balance 114 | updatedAccount1, err := store.GetAccount(context.Background(), account1.ID) 115 | require.NoError(t, err) 116 | require.NotEmpty(t, updatedAccount1) 117 | 118 | updatedAccount2, err := store.GetAccount(context.Background(), account2.ID) 119 | require.NoError(t, err) 120 | require.NotEmpty(t, updatedAccount2) 121 | 122 | fmt.Println(">> updated :::", updatedAccount1.Balance, updatedAccount2.Balance) 123 | require.Equal(t, account1.Balance-int64(n)*amount, updatedAccount1.Balance) 124 | require.Equal(t, account2.Balance+int64(n)*amount, updatedAccount2.Balance) 125 | } 126 | 127 | func TestTransferTxDeadlock(t *testing.T) { 128 | store := NewStore(testDB) 129 | 130 | account1 := createRandomAccount(t) 131 | account2 := createRandomAccount(t) 132 | fmt.Println(">> Before :::", account1.Balance, account2.Balance) 133 | 134 | // run n concurrent transfer transactions 135 | n := 10 136 | amount := int64(10) 137 | 138 | // channels for receiving errors and results from transaction gorutines 139 | errs := make(chan error) 140 | 141 | // execute transactions in goroutines and 142 | // send errors and results back to the main gorutine for validations 143 | for i := 0; i < n; i++ { 144 | fromAccountID := account1.ID 145 | toAccountID := account2.ID 146 | 147 | if i%2 == 1 { 148 | fromAccountID = account2.ID 149 | toAccountID = account1.ID 150 | } 151 | 152 | go func() { 153 | _, err := store.TransferTx(context.Background(), TransferTxParams{ 154 | FromAccountID: fromAccountID, 155 | ToAccountID: toAccountID, 156 | Amount: amount, 157 | }) 158 | 159 | errs <- err 160 | }() 161 | } 162 | 163 | // check results 164 | for i := 0; i < n; i++ { 165 | err := <-errs 166 | require.NoError(t, err) 167 | } 168 | 169 | // check final updated account balance 170 | updatedAccount1, err := store.GetAccount(context.Background(), account1.ID) 171 | require.NoError(t, err) 172 | require.NotEmpty(t, updatedAccount1) 173 | 174 | updatedAccount2, err := store.GetAccount(context.Background(), account2.ID) 175 | require.NoError(t, err) 176 | require.NotEmpty(t, updatedAccount2) 177 | 178 | fmt.Println(">> updated :::", updatedAccount1.Balance, updatedAccount2.Balance) 179 | require.Equal(t, account1.Balance, updatedAccount1.Balance) 180 | require.Equal(t, account2.Balance, updatedAccount2.Balance) 181 | } 182 | -------------------------------------------------------------------------------- /api/account.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/lib/pq" 11 | db "github.com/samirprakash/go-bank/db/sqlc" 12 | "github.com/samirprakash/go-bank/token" 13 | ) 14 | 15 | type createAccountRequest struct { 16 | Currency string `json:"currency" binding:"required,customCurrencyValidator"` 17 | } 18 | 19 | func (server *Server) createAccount(ctx *gin.Context) { 20 | var req createAccountRequest 21 | 22 | // validate request params 23 | if err := ctx.ShouldBindJSON(&req); err != nil { 24 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 25 | return 26 | } 27 | 28 | // add auth to create account 29 | authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) 30 | // create db params 31 | arg := db.CreateAccountParams{ 32 | Owner: authPayload.Username, 33 | Balance: 0, 34 | Currency: req.Currency, 35 | } 36 | 37 | // save to db 38 | account, err := server.store.CreateAccount(ctx, arg) 39 | if err != nil { 40 | if pqErr, ok := err.(*pq.Error); ok { 41 | switch pqErr.Code.Name() { 42 | case "foreign_key_violation", "unique_violation": 43 | ctx.JSON(http.StatusForbidden, errorResponse(err)) 44 | return 45 | } 46 | } 47 | 48 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 49 | return 50 | } 51 | 52 | // return response 53 | ctx.JSON(http.StatusOK, account) 54 | } 55 | 56 | type getAccountRequest struct { 57 | ID int64 `uri:"id" binding:"required,min=1"` 58 | } 59 | 60 | func (server *Server) getAccount(ctx *gin.Context) { 61 | var req getAccountRequest 62 | 63 | // validate request 64 | if err := ctx.ShouldBindUri(&req); err != nil { 65 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 66 | return 67 | } 68 | 69 | // get account from db 70 | account, err := server.store.GetAccount(ctx, req.ID) 71 | if err != nil { 72 | // if account does not exist 73 | if err == sql.ErrNoRows { 74 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 75 | return 76 | } 77 | 78 | // if there is an issue on the server 79 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 80 | return 81 | } 82 | 83 | // add auth middleware 84 | authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) 85 | if account.Owner != authPayload.Username { 86 | err = errors.New("account does not belong to the authenticated user") 87 | ctx.JSON(http.StatusUnauthorized, errorResponse(err)) 88 | return 89 | } 90 | 91 | // return the account 92 | ctx.JSON(http.StatusOK, account) 93 | } 94 | 95 | type listAccountsRequest struct { 96 | PageID int32 `form:"page_id" binding:"required,min=1"` 97 | PageSize int32 `form:"page_size" binding:"required,min=5,max=10"` 98 | } 99 | 100 | func (server *Server) listAccounts(ctx *gin.Context) { 101 | var req listAccountsRequest 102 | 103 | // validate query params 104 | if err := ctx.ShouldBindQuery(&req); err != nil { 105 | // return 400 if bad request 106 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 107 | return 108 | } 109 | 110 | // add auth middleware 111 | authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) 112 | arg := db.ListAccountsParams{ 113 | Owner: authPayload.Username, 114 | Limit: req.PageSize, 115 | Offset: (req.PageID - 1) * req.PageSize, 116 | } 117 | 118 | // get all accounts as per limit and offset 119 | accounts, err := server.store.ListAccounts(ctx, arg) 120 | if err != nil { 121 | // return 500 if internal error 122 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 123 | return 124 | } 125 | 126 | // return the list of accounts 127 | ctx.JSON(http.StatusOK, accounts) 128 | } 129 | 130 | type updateAccountBalanceRequest struct { 131 | Amount int64 `json:"amount" binding:"required,min=1,max=10000"` 132 | } 133 | 134 | func (server *Server) updateAccountBalance(ctx *gin.Context) { 135 | var req updateAccountBalanceRequest 136 | 137 | // validate path param id 138 | id, err := strconv.Atoi(ctx.Param("id")) 139 | if err != nil { 140 | // return 400 if bad request 141 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 142 | return 143 | } 144 | 145 | // Account ID must not be less than 1 146 | if id < 1 { 147 | // return 400 if bad request 148 | ctx.JSON(http.StatusBadRequest, "Invalid ID") 149 | return 150 | } 151 | 152 | // validate requets body 153 | if err := ctx.ShouldBindJSON(&req); err != nil { 154 | // return 400 if bad request 155 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 156 | return 157 | } 158 | 159 | arg := db.UpdateAccountBalanceParams{ 160 | Amount: req.Amount, 161 | ID: int64(id), 162 | } 163 | 164 | // update account balance in db 165 | account, err := server.store.UpdateAccountBalance(ctx, arg) 166 | if err != nil { 167 | if err == sql.ErrNoRows { 168 | // reeturn 404 if account not found 169 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 170 | return 171 | } 172 | 173 | // return 500 if internal error 174 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 175 | return 176 | } 177 | 178 | // return the account with updated balance 179 | ctx.JSON(http.StatusOK, account) 180 | } 181 | 182 | type deleteAccountRequest struct { 183 | ID int64 `uri:"id" binding:"required,min=1"` 184 | } 185 | 186 | func (server *Server) deleteAccount(ctx *gin.Context) { 187 | var req deleteAccountRequest 188 | 189 | if err := ctx.ShouldBindUri(&req); err != nil { 190 | ctx.JSON(http.StatusBadRequest, errorResponse(err)) 191 | return 192 | } 193 | 194 | _, err := server.store.GetAccount(ctx, req.ID) 195 | if err != nil { 196 | if err == sql.ErrNoRows { 197 | ctx.JSON(http.StatusNotFound, errorResponse(err)) 198 | return 199 | } 200 | 201 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 202 | return 203 | } 204 | 205 | err = server.store.DeleteAccount(ctx, req.ID) 206 | if err != nil { 207 | ctx.JSON(http.StatusInternalServerError, errorResponse(err)) 208 | return 209 | } 210 | 211 | ctx.JSON(http.StatusOK, "Accoun deleted!") 212 | } 213 | -------------------------------------------------------------------------------- /api/user_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/golang/mock/gomock" 16 | "github.com/lib/pq" 17 | mockdb "github.com/samirprakash/go-bank/db/mock" 18 | db "github.com/samirprakash/go-bank/db/sqlc" 19 | "github.com/samirprakash/go-bank/util" 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | type eqCreateUserParamsMatcher struct { 24 | arg db.CreateUserParams 25 | password string 26 | } 27 | 28 | func (e eqCreateUserParamsMatcher) Matches(x interface{}) bool { 29 | arg, ok := x.(db.CreateUserParams) 30 | if !ok { 31 | return false 32 | } 33 | 34 | err := util.CheckPassword(e.password, arg.HashedPassword) 35 | if err != nil { 36 | return false 37 | } 38 | 39 | e.arg.HashedPassword = arg.HashedPassword 40 | return reflect.DeepEqual(e.arg, arg) 41 | } 42 | 43 | func (e eqCreateUserParamsMatcher) String() string { 44 | return fmt.Sprintf("matches arg %v and password %v", e.arg, e.password) 45 | } 46 | 47 | func EqCreateUserParams(arg db.CreateUserParams, password string) gomock.Matcher { 48 | return eqCreateUserParamsMatcher{arg, password} 49 | } 50 | 51 | func TestCreateUserAPI(t *testing.T) { 52 | user, password := randomUser(t) 53 | 54 | testCases := []struct { 55 | name string 56 | body gin.H 57 | buildStubs func(store *mockdb.MockStore) 58 | checkResponse func(recoder *httptest.ResponseRecorder) 59 | }{ 60 | { 61 | name: "OK", 62 | body: gin.H{ 63 | "username": user.Username, 64 | "password": password, 65 | "full_name": user.FullName, 66 | "email": user.Email, 67 | }, 68 | buildStubs: func(store *mockdb.MockStore) { 69 | arg := db.CreateUserParams{ 70 | Username: user.Username, 71 | FullName: user.FullName, 72 | Email: user.Email, 73 | } 74 | store.EXPECT(). 75 | CreateUser(gomock.Any(), EqCreateUserParams(arg, password)). 76 | Times(1). 77 | Return(user, nil) 78 | }, 79 | checkResponse: func(recorder *httptest.ResponseRecorder) { 80 | require.Equal(t, http.StatusOK, recorder.Code) 81 | requireBodyMatchUser(t, recorder.Body, user) 82 | }, 83 | }, 84 | { 85 | name: "InternalError", 86 | body: gin.H{ 87 | "username": user.Username, 88 | "password": password, 89 | "full_name": user.FullName, 90 | "email": user.Email, 91 | }, 92 | buildStubs: func(store *mockdb.MockStore) { 93 | store.EXPECT(). 94 | CreateUser(gomock.Any(), gomock.Any()). 95 | Times(1). 96 | Return(db.User{}, sql.ErrConnDone) 97 | }, 98 | checkResponse: func(recorder *httptest.ResponseRecorder) { 99 | require.Equal(t, http.StatusInternalServerError, recorder.Code) 100 | }, 101 | }, 102 | { 103 | name: "DuplicateUsername", 104 | body: gin.H{ 105 | "username": user.Username, 106 | "password": password, 107 | "full_name": user.FullName, 108 | "email": user.Email, 109 | }, 110 | buildStubs: func(store *mockdb.MockStore) { 111 | store.EXPECT(). 112 | CreateUser(gomock.Any(), gomock.Any()). 113 | Times(1). 114 | Return(db.User{}, &pq.Error{Code: "23505"}) 115 | }, 116 | checkResponse: func(recorder *httptest.ResponseRecorder) { 117 | require.Equal(t, http.StatusForbidden, recorder.Code) 118 | }, 119 | }, 120 | { 121 | name: "InvalidUsername", 122 | body: gin.H{ 123 | "username": "invalid-user#1", 124 | "password": password, 125 | "full_name": user.FullName, 126 | "email": user.Email, 127 | }, 128 | buildStubs: func(store *mockdb.MockStore) { 129 | store.EXPECT(). 130 | CreateUser(gomock.Any(), gomock.Any()). 131 | Times(0) 132 | }, 133 | checkResponse: func(recorder *httptest.ResponseRecorder) { 134 | require.Equal(t, http.StatusBadRequest, recorder.Code) 135 | }, 136 | }, 137 | { 138 | name: "InvalidEmail", 139 | body: gin.H{ 140 | "username": user.Username, 141 | "password": password, 142 | "full_name": user.FullName, 143 | "email": "invalid-email", 144 | }, 145 | buildStubs: func(store *mockdb.MockStore) { 146 | store.EXPECT(). 147 | CreateUser(gomock.Any(), gomock.Any()). 148 | Times(0) 149 | }, 150 | checkResponse: func(recorder *httptest.ResponseRecorder) { 151 | require.Equal(t, http.StatusBadRequest, recorder.Code) 152 | }, 153 | }, 154 | { 155 | name: "TooShortPassword", 156 | body: gin.H{ 157 | "username": user.Username, 158 | "password": "123", 159 | "full_name": user.FullName, 160 | "email": user.Email, 161 | }, 162 | buildStubs: func(store *mockdb.MockStore) { 163 | store.EXPECT(). 164 | CreateUser(gomock.Any(), gomock.Any()). 165 | Times(0) 166 | }, 167 | checkResponse: func(recorder *httptest.ResponseRecorder) { 168 | require.Equal(t, http.StatusBadRequest, recorder.Code) 169 | }, 170 | }, 171 | } 172 | 173 | for i := range testCases { 174 | tc := testCases[i] 175 | 176 | t.Run(tc.name, func(t *testing.T) { 177 | ctrl := gomock.NewController(t) 178 | defer ctrl.Finish() 179 | 180 | store := mockdb.NewMockStore(ctrl) 181 | tc.buildStubs(store) 182 | 183 | server := newTestServer(t, store) 184 | recorder := httptest.NewRecorder() 185 | 186 | // Marshal body data to JSON 187 | data, err := json.Marshal(tc.body) 188 | require.NoError(t, err) 189 | 190 | url := "/users" 191 | request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) 192 | require.NoError(t, err) 193 | 194 | server.router.ServeHTTP(recorder, request) 195 | tc.checkResponse(recorder) 196 | }) 197 | } 198 | } 199 | 200 | func TestLoginUserAPI(t *testing.T) { 201 | user, password := randomUser(t) 202 | 203 | testCases := []struct { 204 | name string 205 | body gin.H 206 | buildStubs func(store *mockdb.MockStore) 207 | checkResponse func(recoder *httptest.ResponseRecorder) 208 | }{ 209 | { 210 | name: "OK", 211 | body: gin.H{ 212 | "username": user.Username, 213 | "password": password, 214 | }, 215 | buildStubs: func(store *mockdb.MockStore) { 216 | store.EXPECT(). 217 | GetUser(gomock.Any(), gomock.Eq(user.Username)). 218 | Times(1). 219 | Return(user, nil) 220 | store.EXPECT(). 221 | CreateSession(gomock.Any(), gomock.Any()). 222 | Times(1) 223 | }, 224 | checkResponse: func(recorder *httptest.ResponseRecorder) { 225 | require.Equal(t, http.StatusOK, recorder.Code) 226 | }, 227 | }, 228 | { 229 | name: "UserNotFound", 230 | body: gin.H{ 231 | "username": "NotFound", 232 | "password": password, 233 | }, 234 | buildStubs: func(store *mockdb.MockStore) { 235 | store.EXPECT(). 236 | GetUser(gomock.Any(), gomock.Any()). 237 | Times(1). 238 | Return(db.User{}, sql.ErrNoRows) 239 | }, 240 | checkResponse: func(recorder *httptest.ResponseRecorder) { 241 | require.Equal(t, http.StatusNotFound, recorder.Code) 242 | }, 243 | }, 244 | { 245 | name: "IncorrectPassword", 246 | body: gin.H{ 247 | "username": user.Username, 248 | "password": "incorrect", 249 | }, 250 | buildStubs: func(store *mockdb.MockStore) { 251 | store.EXPECT(). 252 | GetUser(gomock.Any(), gomock.Eq(user.Username)). 253 | Times(1). 254 | Return(user, nil) 255 | }, 256 | checkResponse: func(recorder *httptest.ResponseRecorder) { 257 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 258 | }, 259 | }, 260 | { 261 | name: "InternalError", 262 | body: gin.H{ 263 | "username": user.Username, 264 | "password": password, 265 | }, 266 | buildStubs: func(store *mockdb.MockStore) { 267 | store.EXPECT(). 268 | GetUser(gomock.Any(), gomock.Any()). 269 | Times(1). 270 | Return(db.User{}, sql.ErrConnDone) 271 | }, 272 | checkResponse: func(recorder *httptest.ResponseRecorder) { 273 | require.Equal(t, http.StatusInternalServerError, recorder.Code) 274 | }, 275 | }, 276 | { 277 | name: "InvalidUsername", 278 | body: gin.H{ 279 | "username": "invalid-user#1", 280 | "password": password, 281 | "full_name": user.FullName, 282 | "email": user.Email, 283 | }, 284 | buildStubs: func(store *mockdb.MockStore) { 285 | store.EXPECT(). 286 | GetUser(gomock.Any(), gomock.Any()). 287 | Times(0) 288 | }, 289 | checkResponse: func(recorder *httptest.ResponseRecorder) { 290 | require.Equal(t, http.StatusBadRequest, recorder.Code) 291 | }, 292 | }, 293 | } 294 | 295 | for i := range testCases { 296 | tc := testCases[i] 297 | 298 | t.Run(tc.name, func(t *testing.T) { 299 | ctrl := gomock.NewController(t) 300 | defer ctrl.Finish() 301 | 302 | store := mockdb.NewMockStore(ctrl) 303 | tc.buildStubs(store) 304 | 305 | server := newTestServer(t, store) 306 | recorder := httptest.NewRecorder() 307 | 308 | // Marshal body data to JSON 309 | data, err := json.Marshal(tc.body) 310 | require.NoError(t, err) 311 | 312 | url := "/users/login" 313 | request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) 314 | require.NoError(t, err) 315 | 316 | server.router.ServeHTTP(recorder, request) 317 | tc.checkResponse(recorder) 318 | }) 319 | } 320 | } 321 | 322 | func randomUser(t *testing.T) (user db.User, password string) { 323 | password = util.RandomString(6) 324 | hashedPassword, err := util.HashPassword(password) 325 | require.NoError(t, err) 326 | 327 | user = db.User{ 328 | Username: util.RandomOwnerName(), 329 | HashedPassword: hashedPassword, 330 | FullName: util.RandomOwnerName(), 331 | Email: util.RandomEmail(), 332 | } 333 | return 334 | } 335 | 336 | func requireBodyMatchUser(t *testing.T, body *bytes.Buffer, user db.User) { 337 | data, err := ioutil.ReadAll(body) 338 | require.NoError(t, err) 339 | 340 | var gotUser db.User 341 | err = json.Unmarshal(data, &gotUser) 342 | 343 | require.NoError(t, err) 344 | require.Equal(t, user.Username, gotUser.Username) 345 | require.Equal(t, user.FullName, gotUser.FullName) 346 | require.Equal(t, user.Email, gotUser.Email) 347 | require.Empty(t, gotUser.HashedPassword) 348 | } 349 | -------------------------------------------------------------------------------- /db/mockdb/store.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/samirprakash/go-bank/db/sqlc (interfaces: Store) 3 | 4 | // Package mockdb is a generated GoMock package. 5 | package mockdb 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | db "github.com/samirprakash/go-bank/db/sqlc" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockStore is a mock of Store interface 15 | type MockStore struct { 16 | ctrl *gomock.Controller 17 | recorder *MockStoreMockRecorder 18 | } 19 | 20 | // MockStoreMockRecorder is the mock recorder for MockStore 21 | type MockStoreMockRecorder struct { 22 | mock *MockStore 23 | } 24 | 25 | // NewMockStore creates a new mock instance 26 | func NewMockStore(ctrl *gomock.Controller) *MockStore { 27 | mock := &MockStore{ctrl: ctrl} 28 | mock.recorder = &MockStoreMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockStore) EXPECT() *MockStoreMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // CreateAccount mocks base method 38 | func (m *MockStore) CreateAccount(arg0 context.Context, arg1 db.CreateAccountParams) (db.Account, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "CreateAccount", arg0, arg1) 41 | ret0, _ := ret[0].(db.Account) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // CreateAccount indicates an expected call of CreateAccount 47 | func (mr *MockStoreMockRecorder) CreateAccount(arg0, arg1 interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccount", reflect.TypeOf((*MockStore)(nil).CreateAccount), arg0, arg1) 50 | } 51 | 52 | // CreateEntry mocks base method 53 | func (m *MockStore) CreateEntry(arg0 context.Context, arg1 db.CreateEntryParams) (db.Entry, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "CreateEntry", arg0, arg1) 56 | ret0, _ := ret[0].(db.Entry) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // CreateEntry indicates an expected call of CreateEntry 62 | func (mr *MockStoreMockRecorder) CreateEntry(arg0, arg1 interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEntry", reflect.TypeOf((*MockStore)(nil).CreateEntry), arg0, arg1) 65 | } 66 | 67 | // CreateTransfer mocks base method 68 | func (m *MockStore) CreateTransfer(arg0 context.Context, arg1 db.CreateTransferParams) (db.Transfer, error) { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "CreateTransfer", arg0, arg1) 71 | ret0, _ := ret[0].(db.Transfer) 72 | ret1, _ := ret[1].(error) 73 | return ret0, ret1 74 | } 75 | 76 | // CreateTransfer indicates an expected call of CreateTransfer 77 | func (mr *MockStoreMockRecorder) CreateTransfer(arg0, arg1 interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransfer", reflect.TypeOf((*MockStore)(nil).CreateTransfer), arg0, arg1) 80 | } 81 | 82 | // DeleteAccount mocks base method 83 | func (m *MockStore) DeleteAccount(arg0 context.Context, arg1 int64) error { 84 | m.ctrl.T.Helper() 85 | ret := m.ctrl.Call(m, "DeleteAccount", arg0, arg1) 86 | ret0, _ := ret[0].(error) 87 | return ret0 88 | } 89 | 90 | // DeleteAccount indicates an expected call of DeleteAccount 91 | func (mr *MockStoreMockRecorder) DeleteAccount(arg0, arg1 interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccount", reflect.TypeOf((*MockStore)(nil).DeleteAccount), arg0, arg1) 94 | } 95 | 96 | // DeleteEntry mocks base method 97 | func (m *MockStore) DeleteEntry(arg0 context.Context, arg1 int64) error { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "DeleteEntry", arg0, arg1) 100 | ret0, _ := ret[0].(error) 101 | return ret0 102 | } 103 | 104 | // DeleteEntry indicates an expected call of DeleteEntry 105 | func (mr *MockStoreMockRecorder) DeleteEntry(arg0, arg1 interface{}) *gomock.Call { 106 | mr.mock.ctrl.T.Helper() 107 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEntry", reflect.TypeOf((*MockStore)(nil).DeleteEntry), arg0, arg1) 108 | } 109 | 110 | // DeleteTransfer mocks base method 111 | func (m *MockStore) DeleteTransfer(arg0 context.Context, arg1 int64) error { 112 | m.ctrl.T.Helper() 113 | ret := m.ctrl.Call(m, "DeleteTransfer", arg0, arg1) 114 | ret0, _ := ret[0].(error) 115 | return ret0 116 | } 117 | 118 | // DeleteTransfer indicates an expected call of DeleteTransfer 119 | func (mr *MockStoreMockRecorder) DeleteTransfer(arg0, arg1 interface{}) *gomock.Call { 120 | mr.mock.ctrl.T.Helper() 121 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransfer", reflect.TypeOf((*MockStore)(nil).DeleteTransfer), arg0, arg1) 122 | } 123 | 124 | // GetAccount mocks base method 125 | func (m *MockStore) GetAccount(arg0 context.Context, arg1 int64) (db.Account, error) { 126 | m.ctrl.T.Helper() 127 | ret := m.ctrl.Call(m, "GetAccount", arg0, arg1) 128 | ret0, _ := ret[0].(db.Account) 129 | ret1, _ := ret[1].(error) 130 | return ret0, ret1 131 | } 132 | 133 | // GetAccount indicates an expected call of GetAccount 134 | func (mr *MockStoreMockRecorder) GetAccount(arg0, arg1 interface{}) *gomock.Call { 135 | mr.mock.ctrl.T.Helper() 136 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockStore)(nil).GetAccount), arg0, arg1) 137 | } 138 | 139 | // GetAccountForUpdate mocks base method 140 | func (m *MockStore) GetAccountForUpdate(arg0 context.Context, arg1 int64) (db.Account, error) { 141 | m.ctrl.T.Helper() 142 | ret := m.ctrl.Call(m, "GetAccountForUpdate", arg0, arg1) 143 | ret0, _ := ret[0].(db.Account) 144 | ret1, _ := ret[1].(error) 145 | return ret0, ret1 146 | } 147 | 148 | // GetAccountForUpdate indicates an expected call of GetAccountForUpdate 149 | func (mr *MockStoreMockRecorder) GetAccountForUpdate(arg0, arg1 interface{}) *gomock.Call { 150 | mr.mock.ctrl.T.Helper() 151 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountForUpdate", reflect.TypeOf((*MockStore)(nil).GetAccountForUpdate), arg0, arg1) 152 | } 153 | 154 | // GetEntry mocks base method 155 | func (m *MockStore) GetEntry(arg0 context.Context, arg1 int64) (db.Entry, error) { 156 | m.ctrl.T.Helper() 157 | ret := m.ctrl.Call(m, "GetEntry", arg0, arg1) 158 | ret0, _ := ret[0].(db.Entry) 159 | ret1, _ := ret[1].(error) 160 | return ret0, ret1 161 | } 162 | 163 | // GetEntry indicates an expected call of GetEntry 164 | func (mr *MockStoreMockRecorder) GetEntry(arg0, arg1 interface{}) *gomock.Call { 165 | mr.mock.ctrl.T.Helper() 166 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntry", reflect.TypeOf((*MockStore)(nil).GetEntry), arg0, arg1) 167 | } 168 | 169 | // GetTransfer mocks base method 170 | func (m *MockStore) GetTransfer(arg0 context.Context, arg1 int64) (db.Transfer, error) { 171 | m.ctrl.T.Helper() 172 | ret := m.ctrl.Call(m, "GetTransfer", arg0, arg1) 173 | ret0, _ := ret[0].(db.Transfer) 174 | ret1, _ := ret[1].(error) 175 | return ret0, ret1 176 | } 177 | 178 | // GetTransfer indicates an expected call of GetTransfer 179 | func (mr *MockStoreMockRecorder) GetTransfer(arg0, arg1 interface{}) *gomock.Call { 180 | mr.mock.ctrl.T.Helper() 181 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransfer", reflect.TypeOf((*MockStore)(nil).GetTransfer), arg0, arg1) 182 | } 183 | 184 | // ListAccounts mocks base method 185 | func (m *MockStore) ListAccounts(arg0 context.Context, arg1 db.ListAccountsParams) ([]db.Account, error) { 186 | m.ctrl.T.Helper() 187 | ret := m.ctrl.Call(m, "ListAccounts", arg0, arg1) 188 | ret0, _ := ret[0].([]db.Account) 189 | ret1, _ := ret[1].(error) 190 | return ret0, ret1 191 | } 192 | 193 | // ListAccounts indicates an expected call of ListAccounts 194 | func (mr *MockStoreMockRecorder) ListAccounts(arg0, arg1 interface{}) *gomock.Call { 195 | mr.mock.ctrl.T.Helper() 196 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccounts", reflect.TypeOf((*MockStore)(nil).ListAccounts), arg0, arg1) 197 | } 198 | 199 | // ListEntries mocks base method 200 | func (m *MockStore) ListEntries(arg0 context.Context, arg1 db.ListEntriesParams) ([]db.Entry, error) { 201 | m.ctrl.T.Helper() 202 | ret := m.ctrl.Call(m, "ListEntries", arg0, arg1) 203 | ret0, _ := ret[0].([]db.Entry) 204 | ret1, _ := ret[1].(error) 205 | return ret0, ret1 206 | } 207 | 208 | // ListEntries indicates an expected call of ListEntries 209 | func (mr *MockStoreMockRecorder) ListEntries(arg0, arg1 interface{}) *gomock.Call { 210 | mr.mock.ctrl.T.Helper() 211 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEntries", reflect.TypeOf((*MockStore)(nil).ListEntries), arg0, arg1) 212 | } 213 | 214 | // ListTransfers mocks base method 215 | func (m *MockStore) ListTransfers(arg0 context.Context, arg1 db.ListTransfersParams) ([]db.Transfer, error) { 216 | m.ctrl.T.Helper() 217 | ret := m.ctrl.Call(m, "ListTransfers", arg0, arg1) 218 | ret0, _ := ret[0].([]db.Transfer) 219 | ret1, _ := ret[1].(error) 220 | return ret0, ret1 221 | } 222 | 223 | // ListTransfers indicates an expected call of ListTransfers 224 | func (mr *MockStoreMockRecorder) ListTransfers(arg0, arg1 interface{}) *gomock.Call { 225 | mr.mock.ctrl.T.Helper() 226 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTransfers", reflect.TypeOf((*MockStore)(nil).ListTransfers), arg0, arg1) 227 | } 228 | 229 | // TransferTx mocks base method 230 | func (m *MockStore) TransferTx(arg0 context.Context, arg1 db.TransferTxParams) (db.TransferTxResult, error) { 231 | m.ctrl.T.Helper() 232 | ret := m.ctrl.Call(m, "TransferTx", arg0, arg1) 233 | ret0, _ := ret[0].(db.TransferTxResult) 234 | ret1, _ := ret[1].(error) 235 | return ret0, ret1 236 | } 237 | 238 | // TransferTx indicates an expected call of TransferTx 239 | func (mr *MockStoreMockRecorder) TransferTx(arg0, arg1 interface{}) *gomock.Call { 240 | mr.mock.ctrl.T.Helper() 241 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransferTx", reflect.TypeOf((*MockStore)(nil).TransferTx), arg0, arg1) 242 | } 243 | 244 | // UpdateAccount mocks base method 245 | func (m *MockStore) UpdateAccount(arg0 context.Context, arg1 db.UpdateAccountParams) (db.Account, error) { 246 | m.ctrl.T.Helper() 247 | ret := m.ctrl.Call(m, "UpdateAccount", arg0, arg1) 248 | ret0, _ := ret[0].(db.Account) 249 | ret1, _ := ret[1].(error) 250 | return ret0, ret1 251 | } 252 | 253 | // UpdateAccount indicates an expected call of UpdateAccount 254 | func (mr *MockStoreMockRecorder) UpdateAccount(arg0, arg1 interface{}) *gomock.Call { 255 | mr.mock.ctrl.T.Helper() 256 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccount", reflect.TypeOf((*MockStore)(nil).UpdateAccount), arg0, arg1) 257 | } 258 | 259 | // UpdateAccountBalance mocks base method 260 | func (m *MockStore) UpdateAccountBalance(arg0 context.Context, arg1 db.UpdateAccountBalanceParams) (db.Account, error) { 261 | m.ctrl.T.Helper() 262 | ret := m.ctrl.Call(m, "UpdateAccountBalance", arg0, arg1) 263 | ret0, _ := ret[0].(db.Account) 264 | ret1, _ := ret[1].(error) 265 | return ret0, ret1 266 | } 267 | 268 | // UpdateAccountBalance indicates an expected call of UpdateAccountBalance 269 | func (mr *MockStoreMockRecorder) UpdateAccountBalance(arg0, arg1 interface{}) *gomock.Call { 270 | mr.mock.ctrl.T.Helper() 271 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountBalance", reflect.TypeOf((*MockStore)(nil).UpdateAccountBalance), arg0, arg1) 272 | } 273 | 274 | // UpdateEntry mocks base method 275 | func (m *MockStore) UpdateEntry(arg0 context.Context, arg1 db.UpdateEntryParams) (db.Entry, error) { 276 | m.ctrl.T.Helper() 277 | ret := m.ctrl.Call(m, "UpdateEntry", arg0, arg1) 278 | ret0, _ := ret[0].(db.Entry) 279 | ret1, _ := ret[1].(error) 280 | return ret0, ret1 281 | } 282 | 283 | // UpdateEntry indicates an expected call of UpdateEntry 284 | func (mr *MockStoreMockRecorder) UpdateEntry(arg0, arg1 interface{}) *gomock.Call { 285 | mr.mock.ctrl.T.Helper() 286 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEntry", reflect.TypeOf((*MockStore)(nil).UpdateEntry), arg0, arg1) 287 | } 288 | 289 | // UpdateTransfer mocks base method 290 | func (m *MockStore) UpdateTransfer(arg0 context.Context, arg1 db.UpdateTransferParams) (db.Transfer, error) { 291 | m.ctrl.T.Helper() 292 | ret := m.ctrl.Call(m, "UpdateTransfer", arg0, arg1) 293 | ret0, _ := ret[0].(db.Transfer) 294 | ret1, _ := ret[1].(error) 295 | return ret0, ret1 296 | } 297 | 298 | // UpdateTransfer indicates an expected call of UpdateTransfer 299 | func (mr *MockStoreMockRecorder) UpdateTransfer(arg0, arg1 interface{}) *gomock.Call { 300 | mr.mock.ctrl.T.Helper() 301 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransfer", reflect.TypeOf((*MockStore)(nil).UpdateTransfer), arg0, arg1) 302 | } 303 | -------------------------------------------------------------------------------- /db/mock/store.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/samirprakash/go-bank/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 | gomock "github.com/golang/mock/gomock" 12 | uuid "github.com/google/uuid" 13 | db "github.com/samirprakash/go-bank/db/sqlc" 14 | ) 15 | 16 | // MockStore is a mock of Store interface. 17 | type MockStore struct { 18 | ctrl *gomock.Controller 19 | recorder *MockStoreMockRecorder 20 | } 21 | 22 | // MockStoreMockRecorder is the mock recorder for MockStore. 23 | type MockStoreMockRecorder struct { 24 | mock *MockStore 25 | } 26 | 27 | // NewMockStore creates a new mock instance. 28 | func NewMockStore(ctrl *gomock.Controller) *MockStore { 29 | mock := &MockStore{ctrl: ctrl} 30 | mock.recorder = &MockStoreMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockStore) EXPECT() *MockStoreMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // CreateAccount mocks base method. 40 | func (m *MockStore) CreateAccount(arg0 context.Context, arg1 db.CreateAccountParams) (db.Account, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "CreateAccount", arg0, arg1) 43 | ret0, _ := ret[0].(db.Account) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // CreateAccount indicates an expected call of CreateAccount. 49 | func (mr *MockStoreMockRecorder) CreateAccount(arg0, arg1 interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccount", reflect.TypeOf((*MockStore)(nil).CreateAccount), arg0, arg1) 52 | } 53 | 54 | // CreateEntry mocks base method. 55 | func (m *MockStore) CreateEntry(arg0 context.Context, arg1 db.CreateEntryParams) (db.Entry, error) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "CreateEntry", arg0, arg1) 58 | ret0, _ := ret[0].(db.Entry) 59 | ret1, _ := ret[1].(error) 60 | return ret0, ret1 61 | } 62 | 63 | // CreateEntry indicates an expected call of CreateEntry. 64 | func (mr *MockStoreMockRecorder) CreateEntry(arg0, arg1 interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEntry", reflect.TypeOf((*MockStore)(nil).CreateEntry), arg0, arg1) 67 | } 68 | 69 | // CreateSession mocks base method. 70 | func (m *MockStore) CreateSession(arg0 context.Context, arg1 db.CreateSessionParams) (db.Session, error) { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "CreateSession", arg0, arg1) 73 | ret0, _ := ret[0].(db.Session) 74 | ret1, _ := ret[1].(error) 75 | return ret0, ret1 76 | } 77 | 78 | // CreateSession indicates an expected call of CreateSession. 79 | func (mr *MockStoreMockRecorder) CreateSession(arg0, arg1 interface{}) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockStore)(nil).CreateSession), arg0, arg1) 82 | } 83 | 84 | // CreateTransfer mocks base method. 85 | func (m *MockStore) CreateTransfer(arg0 context.Context, arg1 db.CreateTransferParams) (db.Transfer, error) { 86 | m.ctrl.T.Helper() 87 | ret := m.ctrl.Call(m, "CreateTransfer", arg0, arg1) 88 | ret0, _ := ret[0].(db.Transfer) 89 | ret1, _ := ret[1].(error) 90 | return ret0, ret1 91 | } 92 | 93 | // CreateTransfer indicates an expected call of CreateTransfer. 94 | func (mr *MockStoreMockRecorder) CreateTransfer(arg0, arg1 interface{}) *gomock.Call { 95 | mr.mock.ctrl.T.Helper() 96 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransfer", reflect.TypeOf((*MockStore)(nil).CreateTransfer), arg0, arg1) 97 | } 98 | 99 | // CreateUser mocks base method. 100 | func (m *MockStore) CreateUser(arg0 context.Context, arg1 db.CreateUserParams) (db.User, error) { 101 | m.ctrl.T.Helper() 102 | ret := m.ctrl.Call(m, "CreateUser", arg0, arg1) 103 | ret0, _ := ret[0].(db.User) 104 | ret1, _ := ret[1].(error) 105 | return ret0, ret1 106 | } 107 | 108 | // CreateUser indicates an expected call of CreateUser. 109 | func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call { 110 | mr.mock.ctrl.T.Helper() 111 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0, arg1) 112 | } 113 | 114 | // DeleteAccount mocks base method. 115 | func (m *MockStore) DeleteAccount(arg0 context.Context, arg1 int64) error { 116 | m.ctrl.T.Helper() 117 | ret := m.ctrl.Call(m, "DeleteAccount", arg0, arg1) 118 | ret0, _ := ret[0].(error) 119 | return ret0 120 | } 121 | 122 | // DeleteAccount indicates an expected call of DeleteAccount. 123 | func (mr *MockStoreMockRecorder) DeleteAccount(arg0, arg1 interface{}) *gomock.Call { 124 | mr.mock.ctrl.T.Helper() 125 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccount", reflect.TypeOf((*MockStore)(nil).DeleteAccount), arg0, arg1) 126 | } 127 | 128 | // DeleteEntry mocks base method. 129 | func (m *MockStore) DeleteEntry(arg0 context.Context, arg1 int64) error { 130 | m.ctrl.T.Helper() 131 | ret := m.ctrl.Call(m, "DeleteEntry", arg0, arg1) 132 | ret0, _ := ret[0].(error) 133 | return ret0 134 | } 135 | 136 | // DeleteEntry indicates an expected call of DeleteEntry. 137 | func (mr *MockStoreMockRecorder) DeleteEntry(arg0, arg1 interface{}) *gomock.Call { 138 | mr.mock.ctrl.T.Helper() 139 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEntry", reflect.TypeOf((*MockStore)(nil).DeleteEntry), arg0, arg1) 140 | } 141 | 142 | // DeleteTransfer mocks base method. 143 | func (m *MockStore) DeleteTransfer(arg0 context.Context, arg1 int64) error { 144 | m.ctrl.T.Helper() 145 | ret := m.ctrl.Call(m, "DeleteTransfer", arg0, arg1) 146 | ret0, _ := ret[0].(error) 147 | return ret0 148 | } 149 | 150 | // DeleteTransfer indicates an expected call of DeleteTransfer. 151 | func (mr *MockStoreMockRecorder) DeleteTransfer(arg0, arg1 interface{}) *gomock.Call { 152 | mr.mock.ctrl.T.Helper() 153 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransfer", reflect.TypeOf((*MockStore)(nil).DeleteTransfer), arg0, arg1) 154 | } 155 | 156 | // GetAccount mocks base method. 157 | func (m *MockStore) GetAccount(arg0 context.Context, arg1 int64) (db.Account, error) { 158 | m.ctrl.T.Helper() 159 | ret := m.ctrl.Call(m, "GetAccount", arg0, arg1) 160 | ret0, _ := ret[0].(db.Account) 161 | ret1, _ := ret[1].(error) 162 | return ret0, ret1 163 | } 164 | 165 | // GetAccount indicates an expected call of GetAccount. 166 | func (mr *MockStoreMockRecorder) GetAccount(arg0, arg1 interface{}) *gomock.Call { 167 | mr.mock.ctrl.T.Helper() 168 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockStore)(nil).GetAccount), arg0, arg1) 169 | } 170 | 171 | // GetAccountForUpdate mocks base method. 172 | func (m *MockStore) GetAccountForUpdate(arg0 context.Context, arg1 int64) (db.Account, error) { 173 | m.ctrl.T.Helper() 174 | ret := m.ctrl.Call(m, "GetAccountForUpdate", arg0, arg1) 175 | ret0, _ := ret[0].(db.Account) 176 | ret1, _ := ret[1].(error) 177 | return ret0, ret1 178 | } 179 | 180 | // GetAccountForUpdate indicates an expected call of GetAccountForUpdate. 181 | func (mr *MockStoreMockRecorder) GetAccountForUpdate(arg0, arg1 interface{}) *gomock.Call { 182 | mr.mock.ctrl.T.Helper() 183 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountForUpdate", reflect.TypeOf((*MockStore)(nil).GetAccountForUpdate), arg0, arg1) 184 | } 185 | 186 | // GetEntry mocks base method. 187 | func (m *MockStore) GetEntry(arg0 context.Context, arg1 int64) (db.Entry, error) { 188 | m.ctrl.T.Helper() 189 | ret := m.ctrl.Call(m, "GetEntry", arg0, arg1) 190 | ret0, _ := ret[0].(db.Entry) 191 | ret1, _ := ret[1].(error) 192 | return ret0, ret1 193 | } 194 | 195 | // GetEntry indicates an expected call of GetEntry. 196 | func (mr *MockStoreMockRecorder) GetEntry(arg0, arg1 interface{}) *gomock.Call { 197 | mr.mock.ctrl.T.Helper() 198 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntry", reflect.TypeOf((*MockStore)(nil).GetEntry), arg0, arg1) 199 | } 200 | 201 | // GetSession mocks base method. 202 | func (m *MockStore) GetSession(arg0 context.Context, arg1 uuid.UUID) (db.Session, error) { 203 | m.ctrl.T.Helper() 204 | ret := m.ctrl.Call(m, "GetSession", arg0, arg1) 205 | ret0, _ := ret[0].(db.Session) 206 | ret1, _ := ret[1].(error) 207 | return ret0, ret1 208 | } 209 | 210 | // GetSession indicates an expected call of GetSession. 211 | func (mr *MockStoreMockRecorder) GetSession(arg0, arg1 interface{}) *gomock.Call { 212 | mr.mock.ctrl.T.Helper() 213 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockStore)(nil).GetSession), arg0, arg1) 214 | } 215 | 216 | // GetTransfer mocks base method. 217 | func (m *MockStore) GetTransfer(arg0 context.Context, arg1 int64) (db.Transfer, error) { 218 | m.ctrl.T.Helper() 219 | ret := m.ctrl.Call(m, "GetTransfer", arg0, arg1) 220 | ret0, _ := ret[0].(db.Transfer) 221 | ret1, _ := ret[1].(error) 222 | return ret0, ret1 223 | } 224 | 225 | // GetTransfer indicates an expected call of GetTransfer. 226 | func (mr *MockStoreMockRecorder) GetTransfer(arg0, arg1 interface{}) *gomock.Call { 227 | mr.mock.ctrl.T.Helper() 228 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransfer", reflect.TypeOf((*MockStore)(nil).GetTransfer), arg0, arg1) 229 | } 230 | 231 | // GetUser mocks base method. 232 | func (m *MockStore) GetUser(arg0 context.Context, arg1 string) (db.User, error) { 233 | m.ctrl.T.Helper() 234 | ret := m.ctrl.Call(m, "GetUser", arg0, arg1) 235 | ret0, _ := ret[0].(db.User) 236 | ret1, _ := ret[1].(error) 237 | return ret0, ret1 238 | } 239 | 240 | // GetUser indicates an expected call of GetUser. 241 | func (mr *MockStoreMockRecorder) GetUser(arg0, arg1 interface{}) *gomock.Call { 242 | mr.mock.ctrl.T.Helper() 243 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockStore)(nil).GetUser), arg0, arg1) 244 | } 245 | 246 | // ListAccounts mocks base method. 247 | func (m *MockStore) ListAccounts(arg0 context.Context, arg1 db.ListAccountsParams) ([]db.Account, error) { 248 | m.ctrl.T.Helper() 249 | ret := m.ctrl.Call(m, "ListAccounts", arg0, arg1) 250 | ret0, _ := ret[0].([]db.Account) 251 | ret1, _ := ret[1].(error) 252 | return ret0, ret1 253 | } 254 | 255 | // ListAccounts indicates an expected call of ListAccounts. 256 | func (mr *MockStoreMockRecorder) ListAccounts(arg0, arg1 interface{}) *gomock.Call { 257 | mr.mock.ctrl.T.Helper() 258 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccounts", reflect.TypeOf((*MockStore)(nil).ListAccounts), arg0, arg1) 259 | } 260 | 261 | // ListEntries mocks base method. 262 | func (m *MockStore) ListEntries(arg0 context.Context, arg1 db.ListEntriesParams) ([]db.Entry, error) { 263 | m.ctrl.T.Helper() 264 | ret := m.ctrl.Call(m, "ListEntries", arg0, arg1) 265 | ret0, _ := ret[0].([]db.Entry) 266 | ret1, _ := ret[1].(error) 267 | return ret0, ret1 268 | } 269 | 270 | // ListEntries indicates an expected call of ListEntries. 271 | func (mr *MockStoreMockRecorder) ListEntries(arg0, arg1 interface{}) *gomock.Call { 272 | mr.mock.ctrl.T.Helper() 273 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEntries", reflect.TypeOf((*MockStore)(nil).ListEntries), arg0, arg1) 274 | } 275 | 276 | // ListTransfers mocks base method. 277 | func (m *MockStore) ListTransfers(arg0 context.Context, arg1 db.ListTransfersParams) ([]db.Transfer, error) { 278 | m.ctrl.T.Helper() 279 | ret := m.ctrl.Call(m, "ListTransfers", arg0, arg1) 280 | ret0, _ := ret[0].([]db.Transfer) 281 | ret1, _ := ret[1].(error) 282 | return ret0, ret1 283 | } 284 | 285 | // ListTransfers indicates an expected call of ListTransfers. 286 | func (mr *MockStoreMockRecorder) ListTransfers(arg0, arg1 interface{}) *gomock.Call { 287 | mr.mock.ctrl.T.Helper() 288 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTransfers", reflect.TypeOf((*MockStore)(nil).ListTransfers), arg0, arg1) 289 | } 290 | 291 | // TransferTx mocks base method. 292 | func (m *MockStore) TransferTx(arg0 context.Context, arg1 db.TransferTxParams) (db.TransferTxResult, error) { 293 | m.ctrl.T.Helper() 294 | ret := m.ctrl.Call(m, "TransferTx", arg0, arg1) 295 | ret0, _ := ret[0].(db.TransferTxResult) 296 | ret1, _ := ret[1].(error) 297 | return ret0, ret1 298 | } 299 | 300 | // TransferTx indicates an expected call of TransferTx. 301 | func (mr *MockStoreMockRecorder) TransferTx(arg0, arg1 interface{}) *gomock.Call { 302 | mr.mock.ctrl.T.Helper() 303 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransferTx", reflect.TypeOf((*MockStore)(nil).TransferTx), arg0, arg1) 304 | } 305 | 306 | // UpdateAccount mocks base method. 307 | func (m *MockStore) UpdateAccount(arg0 context.Context, arg1 db.UpdateAccountParams) (db.Account, error) { 308 | m.ctrl.T.Helper() 309 | ret := m.ctrl.Call(m, "UpdateAccount", arg0, arg1) 310 | ret0, _ := ret[0].(db.Account) 311 | ret1, _ := ret[1].(error) 312 | return ret0, ret1 313 | } 314 | 315 | // UpdateAccount indicates an expected call of UpdateAccount. 316 | func (mr *MockStoreMockRecorder) UpdateAccount(arg0, arg1 interface{}) *gomock.Call { 317 | mr.mock.ctrl.T.Helper() 318 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccount", reflect.TypeOf((*MockStore)(nil).UpdateAccount), arg0, arg1) 319 | } 320 | 321 | // UpdateAccountBalance mocks base method. 322 | func (m *MockStore) UpdateAccountBalance(arg0 context.Context, arg1 db.UpdateAccountBalanceParams) (db.Account, error) { 323 | m.ctrl.T.Helper() 324 | ret := m.ctrl.Call(m, "UpdateAccountBalance", arg0, arg1) 325 | ret0, _ := ret[0].(db.Account) 326 | ret1, _ := ret[1].(error) 327 | return ret0, ret1 328 | } 329 | 330 | // UpdateAccountBalance indicates an expected call of UpdateAccountBalance. 331 | func (mr *MockStoreMockRecorder) UpdateAccountBalance(arg0, arg1 interface{}) *gomock.Call { 332 | mr.mock.ctrl.T.Helper() 333 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountBalance", reflect.TypeOf((*MockStore)(nil).UpdateAccountBalance), arg0, arg1) 334 | } 335 | 336 | // UpdateEntry mocks base method. 337 | func (m *MockStore) UpdateEntry(arg0 context.Context, arg1 db.UpdateEntryParams) (db.Entry, error) { 338 | m.ctrl.T.Helper() 339 | ret := m.ctrl.Call(m, "UpdateEntry", arg0, arg1) 340 | ret0, _ := ret[0].(db.Entry) 341 | ret1, _ := ret[1].(error) 342 | return ret0, ret1 343 | } 344 | 345 | // UpdateEntry indicates an expected call of UpdateEntry. 346 | func (mr *MockStoreMockRecorder) UpdateEntry(arg0, arg1 interface{}) *gomock.Call { 347 | mr.mock.ctrl.T.Helper() 348 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEntry", reflect.TypeOf((*MockStore)(nil).UpdateEntry), arg0, arg1) 349 | } 350 | 351 | // UpdateTransfer mocks base method. 352 | func (m *MockStore) UpdateTransfer(arg0 context.Context, arg1 db.UpdateTransferParams) (db.Transfer, error) { 353 | m.ctrl.T.Helper() 354 | ret := m.ctrl.Call(m, "UpdateTransfer", arg0, arg1) 355 | ret0, _ := ret[0].(db.Transfer) 356 | ret1, _ := ret[1].(error) 357 | return ret0, ret1 358 | } 359 | 360 | // UpdateTransfer indicates an expected call of UpdateTransfer. 361 | func (mr *MockStoreMockRecorder) UpdateTransfer(arg0, arg1 interface{}) *gomock.Call { 362 | mr.mock.ctrl.T.Helper() 363 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransfer", reflect.TypeOf((*MockStore)(nil).UpdateTransfer), arg0, arg1) 364 | } 365 | -------------------------------------------------------------------------------- /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/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= 44 | github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOdbQRg5nAHt2jrc5QbV0AGuhDdfQI6gXjiFE= 45 | github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= 46 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= 47 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= 48 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 49 | github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q= 50 | github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 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 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 54 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 55 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 56 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 57 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 58 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 59 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 60 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 61 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 62 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 63 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 64 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 65 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 66 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 67 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 68 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 69 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 70 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 71 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 72 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 73 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 74 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 75 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 76 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 77 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 78 | github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= 79 | github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= 80 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 81 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 82 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 83 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 84 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 85 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 86 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 87 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 88 | github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ= 89 | github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4= 90 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 91 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 92 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 93 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 94 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 95 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 96 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 97 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 98 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 99 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 100 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 101 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 102 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 103 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 104 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 105 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 106 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 107 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 108 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 109 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 110 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 111 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 112 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 113 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 114 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 115 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 116 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 117 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 118 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 119 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 120 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 121 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 122 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 123 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 124 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 125 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 126 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 127 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 129 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 131 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 132 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 133 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 134 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 135 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 136 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 137 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 138 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 139 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 140 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 141 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 142 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 143 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 144 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 145 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 146 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 147 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 148 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 149 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 150 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 151 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 152 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 153 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 154 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 155 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 156 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 157 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 158 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 159 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 160 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 161 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 162 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 163 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 164 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 165 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 166 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 167 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 168 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 169 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 170 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 171 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 172 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 173 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 174 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 175 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 176 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 177 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 178 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 179 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 180 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 181 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 182 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 183 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 184 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 185 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 186 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 187 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 188 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 189 | github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= 190 | github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= 191 | github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= 192 | github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 193 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 194 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 195 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 196 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 197 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 198 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 199 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 200 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 201 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 202 | github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= 203 | github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 204 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 205 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 206 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 207 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 208 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 209 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 210 | github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= 211 | github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= 212 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 213 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 214 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 215 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 216 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 217 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 218 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 219 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 220 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 221 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 222 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 223 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 224 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 225 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= 226 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 227 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 228 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 229 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 230 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 231 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 232 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 233 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 234 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 235 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 236 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 237 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 238 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 239 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 240 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 241 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 242 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 243 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 244 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 245 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 246 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 247 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 248 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 249 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 250 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 251 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 252 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 253 | golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= 254 | golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= 255 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 256 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 257 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 258 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 259 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 260 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 261 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 262 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 263 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 264 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 265 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 266 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 267 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 268 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 269 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 270 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 271 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 272 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 273 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 274 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 275 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 276 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 277 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 278 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 279 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 280 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 281 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 282 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 283 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 284 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 285 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 286 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 287 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 288 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 289 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 290 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 291 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 292 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 293 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 294 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 295 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 296 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 297 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 298 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 299 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 300 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 301 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 302 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 303 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 304 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 305 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 306 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 307 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 308 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 309 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 310 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 311 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 312 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 313 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 314 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 315 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 316 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 317 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 318 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 319 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 320 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 321 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 322 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= 323 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 324 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 325 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 326 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 327 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 328 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 329 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 330 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 331 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 332 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 333 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 334 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 335 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 336 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 337 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 338 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 339 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 340 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 341 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 342 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 343 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 344 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 345 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 346 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 347 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 351 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 352 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 353 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 354 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 355 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 356 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 357 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 360 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 362 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 363 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 364 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 365 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 366 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 367 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 368 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 369 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 370 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 371 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 372 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 373 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 374 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 375 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 376 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 377 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 378 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 379 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 381 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 382 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 383 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 384 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 385 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 386 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 387 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 388 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 389 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 390 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 391 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 392 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 393 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 394 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 395 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 396 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 397 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 398 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 399 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 400 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 401 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 402 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 403 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 404 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 405 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 406 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 407 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 408 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 409 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 410 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 411 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 412 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 413 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 414 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 415 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 416 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 417 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 418 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 419 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 420 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 421 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 422 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 423 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 424 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 425 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 426 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 427 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 428 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 429 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 430 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 431 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 432 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 433 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 434 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 435 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 436 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 437 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 438 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 439 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 440 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 441 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 442 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 443 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 444 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 445 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 446 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 447 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 448 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 449 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 450 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 451 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 452 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 453 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 454 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 455 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 456 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 457 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 458 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 459 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 460 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 461 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 462 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 463 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 464 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 465 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 466 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 467 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 468 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 469 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 470 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 471 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 472 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 473 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 474 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 475 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 476 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 477 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 478 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 479 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 480 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 481 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 482 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 483 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 484 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 485 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 486 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 487 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 488 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 489 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 490 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 491 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 492 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 493 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 494 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 495 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 496 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 497 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 498 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 499 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 500 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 501 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 502 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 503 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 504 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 505 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 506 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 507 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 508 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 509 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 510 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 511 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 512 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 513 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 514 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 515 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 516 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 517 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 518 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 519 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 520 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 521 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 522 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 523 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 524 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 525 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 526 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 527 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 528 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 529 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 530 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 531 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 532 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 533 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 534 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 535 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 536 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 537 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 538 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 539 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 540 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 541 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 542 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 543 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 544 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 545 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 546 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 547 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 548 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 549 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 550 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 551 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 552 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 553 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 554 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 555 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 556 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 557 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 558 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 559 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 560 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 561 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 562 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 563 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 564 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 565 | --------------------------------------------------------------------------------