techschool / simplebank Goto Github PK
View Code? Open in Web Editor NEWBackend master class: build a simple bank service in Go
License: MIT License
Backend master class: build a simple bank service in Go
License: MIT License
I want to test my grpc server with full list of interceptors
First of all thanks for the excellent lecture!
I have already learned the Lecture #24, and the commit is e2443c3
I like to run tests in parallel, because it can detect race conditions. So I added t.Parallel()
in all test functions and inside t.Run()
.
Then I run go test -race ./...
, race detected, sample output like that:
=== CONT TestGetAccountAPI/UnauthorizedUser
testing.go:1319: race detected during execution of test
[GIN] 2022/09/15 - 11:32:26 | 500 | 532.9µs | | GET "/accounts/972"
[GIN] 2022/09/15 - 11:32:26 | 404 | 0s | | GET "/accounts/972"
--- FAIL: TestGetAccountAPI/UnauthorizedUser (0.01s)
=== RUN TestGetAccountAPI/NoAuthorization
=== PAUSE TestGetAccountAPI/NoAuthorization
=== CONT TestGetAccountAPI/NoAuthorization
github.com/techschool/simplebank/api.newTestServer()
D:/Files/codes/clone/simplebank/api/main_test.go:20 +0xdb
github.com/techschool/simplebank/api.TestGetAccountAPI.func19()
D:/Files/codes/clone/simplebank/api/account_test.go:144 +0x40e
testing.tRunner()
E:/Programs/for_coding/Go/src/testing/testing.go:1446 +0x216
testing.(*T).Run.func1()
E:/Programs/for_coding/Go/src/testing/testing.go:1493 +0x47
Previous write at 0x00c0001d5668 by goroutine 43:
github.com/go-playground/validator/v10.(*Validate).registerValidation()
D:/Files/codes/gopath/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:189 +0x116
github.com/go-playground/validator/v10.(*Validate).RegisterValidationCtx()
D:/Files/codes/gopath/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:173 +0xa4
github.com/go-playground/validator/v10.(*Validate).RegisterValidation()
D:/Files/codes/gopath/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:163 +0x46
github.com/techschool/simplebank/api.NewServer()
D:/Files/codes/clone/simplebank/api/server.go:36 +0x2e9
github.com/techschool/simplebank/api.newTestServer()
D:/Files/codes/clone/simplebank/api/main_test.go:20 +0xdb
github.com/techschool/simplebank/api.TestGetAccountAPI.func19()
D:/Files/codes/clone/simplebank/api/account_test.go:144 +0x40e
testing.tRunner()
E:/Programs/for_coding/Go/src/testing/testing.go:1446 +0x216
testing.(*T).Run.func1()
E:/Programs/for_coding/Go/src/testing/testing.go:1493 +0x47
Then I found v.RegisterValidation("currency", validCurrency)
is not thead-safe, so I added a mutex in NewServer()
function, but it still not working, a sample output like that:
=== CONT TestGetAccountAPI
testing.go:1319: race detected during execution of test
--- FAIL: TestGetAccountAPI (0.42s)
=== RUN TestGetAccountAPI/OK
=== PAUSE TestGetAccountAPI/OK
=== CONT TestGetAccountAPI/OK
github.com/gin-gonic/gin.(*Context).Next()
D:/Files/codes/gopath/pkg/mod/github.com/gin-gonic/[email protected]/context.go:161 +0x14a
github.com/gin-gonic/gin.RecoveryWithWriter.func1()
D:/Files/codes/gopath/pkg/mod/github.com/gin-gonic/[email protected]/recovery.go:83 +0xae
github.com/gin-gonic/gin.(*Context).Next()
D:/Files/codes/gopath/pkg/mod/github.com/gin-gonic/[email protected]/context.go:161 +0x219
github.com/gin-gonic/gin.LoggerWithConfig.func1()
D:/Files/codes/gopath/pkg/mod/github.com/gin-gonic/[email protected]/logger.go:241 +0x189
github.com/gin-gonic/gin.(*Context).Next()
D:/Files/codes/gopath/pkg/mod/github.com/gin-gonic/[email protected]/context.go:161 +0xb63
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest()
D:/Files/codes/gopath/pkg/mod/github.com/gin-gonic/[email protected]/gin.go:409 +0x75b
github.com/gin-gonic/gin.(*Engine).ServeHTTP()
D:/Files/codes/gopath/pkg/mod/github.com/gin-gonic/[email protected]/gin.go:367 +0x367
github.com/techschool/simplebank/api.TestCreateAccountAPI.func13()
D:/Files/codes/clone/simplebank/api/account_test.go:273 +0x771
testing.tRunner()
E:/Programs/for_coding/Go/src/testing/testing.go:1446 +0x216
testing.(*T).Run.func1()
E:/Programs/for_coding/Go/src/testing/testing.go:1493 +0x47
Previous write at 0x00c0004140a8 by goroutine 62:
github.com/go-playground/validator/v10.(*Validate).registerValidation()
D:/Files/codes/gopath/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:189 +0x116
github.com/go-playground/validator/v10.(*Validate).RegisterValidationCtx()
D:/Files/codes/gopath/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:173 +0xa4
github.com/go-playground/validator/v10.(*Validate).RegisterValidation()
D:/Files/codes/gopath/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:163 +0x46
--- PASS: TestGetAccountAPI/OK (0.03s)
It seems that ctx.Next()
in AuthMiddleware
cause the error, so I locked the ctx.Next()
, but the error still exists.
After several debugging, I found that if I delete t.Parallel()
in user_test.go
the error gone.
But the problem still not fixed, and I don't known why.
Could you please help me with that problem? The error is easy to reproduce, just make all tests run in parallel and run go test -race ./...
.
Thanks.
I followed the video to learn how to build the image. When I ran the container, the following error occurred.
$ docker run --rm simplebank:latest
exec /app/start.sh: exec format error
My Dockerfile is as follows
# Build stage
FROM golang:1.21-alpine3.18 AS builder
WORKDIR /app
COPY . .
RUN go build -o main main.go
RUN apk add curl
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz
# Run stage
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/main .
COPY --from=builder /app/migrate ./migrate
COPY app.env .
COPY start.sh .
COPY wait-for.sh .
COPY db/migration ./migration
EXPOSE 8000
CMD [ "/app/main" ]
ENTRYPOINT [ "/app/start.sh" ]
But when I comment out ENTRYPOINT, there are no errors and the debug log of the gin framework can be printed normally.
And when I use the following docker compose to run, this error will not be reported, and the debug log of the gin framework can be printed normally.
version: "3.9"
services:
postgres:
image: postgres:14-alpine
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=simple_bank
ports:
- "5432:5432"
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- DB_SOURCE=postgresql://root:secret@postgres:5432/simple_bank?sslmode=disable
depends_on:
- postgres
entrypoint:
[
"/app/wait-for.sh",
"postgres:5432",
"--",
"/app/start.sh"
]
command: [ "/app/main" ]
My start.sh is as follows, and also granted executable permissions
#/bin/sh
set -e
echo "run db migration"
source /app/app.env
/app/migrate -path /app/migration -database "$DB_SOURCE" -verbose up
echo "start the app"
exec "$@"
I don't know where the error is caused. Look forward to your reply.
PS: OS is centos8
$ docker version
Client: Docker Engine - Community
Version: 24.0.2
API version: 1.43
Go version: go1.20.4
Git commit: cb74dfc
Built: Thu May 25 21:53:10 2023
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 24.0.2
API version: 1.43 (minimum version 1.12)
Go version: go1.20.4
Git commit: 659604f
Built: Thu May 25 21:52:10 2023
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.6.21
GitCommit: 3dce8eb055cbb6872793272b4f20ed16117344f8
runc:
Version: 1.1.7
GitCommit: v1.1.7-0-g860f061
docker-init:
Version: 0.19.0
GitCommit: de40ad0
This my storage.go file
package db
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
//Store provides all functions to execute db queries and transactions
type Store struct {
*Queries
db *pgx.Conn
}
type TransferTxParams struct {
FromAccountID int64 `json:"from_account_id"`
ToAccountID int64 `json:"to_account_id"`
Amount pgtype.Numeric `json:"amount"`
}
type TransferTxResult struct {
Transfer Transfer `json:"transfer"`
FromAccount Account `json:"from_account"`
ToAccount Account `json:"to_account"`
FromEntry Entry `json:"from_entry"`
ToEntry Entry `json:"to_entry"`
}
func NewStore(db *pgx.Conn) *Store {
return &Store{
db: db,
Queries: New(db),
}
}
//executeTranscation executres a function within a database transaction
func (store *Store) executeTransaction(ctx context.Context, fn func(*Queries) error) error {
options := pgx.TxOptions {
IsoLevel: pgx.TxIsoLevel(pgx.Deferrable),
AccessMode: pgx.ReadWrite,
}
transaction, err := store.db.BeginTx(ctx, options)
if err != nil {
return err
}
query := New(transaction)
err = fn(query)
if err != nil {
if rollbackError := transaction.Rollback(ctx); rollbackError != nil {
return fmt.Errorf("transaction error: %v, rollback error: %v", err, rollbackError)
}
return err
}
return transaction.Commit(ctx)
}
func (store *Store) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) {
var result TransferTxResult
err := store.executeTransaction(ctx, func(query *Queries) error {
var err error
transfer := CreateTransferParams(TransferTxParams {
FromAccountID: arg.FromAccountID,
ToAccountID: arg.ToAccountID,
Amount: arg.Amount,
})
result.Transfer, err = query.CreateTransfer(ctx, transfer)
if err != nil {
return err
}
// result.FromEntry, err = query.CreateEntry(ctx, CreateEntryParams {
// AccountID: arg.FromAccountID,
// Amount: -arg.Amount.Int.Int64(),
// })
// if err != nil {
// return err
// }
// result.ToEntry, err = query.CreateEntry(ctx, CreateEntryParams {
// AccountID: arg.ToAccountID,
// Amount: arg.Amount.Int.Int64(),
// })
// if err != nil {
// return err
// }
// // TODO: update account's balance
// account1, err := query.GetAccountForUpdate(ctx, arg.FromAccountID);
// if err != nil {
// return err
// }
// result.FromAccount, err = query.UpdateAccount(ctx, UpdateAccountParams {
// ID: arg.FromAccountID,
// Balance: util.FromIntToPgNumeric(account1.Balance.Int.Int64() - arg.Amount.Int.Int64()),
// })
// if err != nil {
// return err
// }
// account2, err := query.GetAccountForUpdate(ctx, arg.ToAccountID);
// if err != nil {
// return err
// }
// result.ToAccount, err = query.UpdateAccount(ctx, UpdateAccountParams {
// ID: arg.ToAccountID,
// Balance: util.FromIntToPgNumeric(account2.Balance.Int.Int64() + arg.Amount.Int.Int64()),
// })
// if err != nil {
// return err
// }
return nil
})
return result, err
}
And i am getting the following errors
>> before: 579 832
panic: BUG: slow write timer already active
goroutine 37 [running]:
github.com/jackc/pgx/v5/pgconn.(*PgConn).enterPotentialWriteReadDeadlock(...)
/Users/bilalashraf/go/pkg/mod/github.com/jackc/pgx/[email protected]/pgconn/pgconn.go:1833
github.com/jackc/pgx/v5/pgconn.(*PgConn).flushWithPotentialWriteReadDeadlock(0x14000103680)
/Users/bilalashraf/go/pkg/mod/github.com/jackc/pgx/[email protected]/pgconn/pgconn.go:1852 +0xb4
github.com/jackc/pgx/v5/pgconn.(*PgConn).Close(0x14000103680, {0x1009768a0?, 0x140000a8000})
/Users/bilalashraf/go/pkg/mod/github.com/jackc/pgx/[email protected]/pgconn/pgconn.go:640 +0x160
github.com/jackc/pgx/v5.(*Conn).die(0x14000151c20, {0x1009767f8?, 0x100c9ef40?})
/Users/bilalashraf/go/pkg/mod/github.com/jackc/pgx/[email protected]/conn.go:412 +0x74
github.com/jackc/pgx/v5.(*Conn).BeginTx(0x14000151c20, {0x1009767f8, 0x100c9ef40}, {{0x1007eca5c, 0xa}, {0x1007eca66, 0xa}, {0x0, 0x0}, {0x0, ...}})
/Users/bilalashraf/go/pkg/mod/github.com/jackc/pgx/[email protected]/tx.go:104 +0x120
github.com/billalaashraf/simplebank/db/sqlc.(*Store).executeTransaction(0x0?, {0x1009767f8, 0x100c9ef40}, 0x140000b1960)
/Users/bilalashraf/Work/Backend/simplebank/db/sqlc/storage.go:43 +0x80
github.com/billalaashraf/simplebank/db/sqlc.(*Store).TransferTx(_, {_, _}, {_, _, {_, _, _, _, _}})
/Users/bilalashraf/Work/Backend/simplebank/db/sqlc/storage.go:60 +0xc8
github.com/billalaashraf/simplebank/db/sqlc.TestTransferTx.func1()
/Users/bilalashraf/Work/Backend/simplebank/db/sqlc/storage_test.go:29 +0xa0
created by github.com/billalaashraf/simplebank/db/sqlc.TestTransferTx in goroutine 35
/Users/bilalashraf/Work/Backend/simplebank/db/sqlc/storage_test.go:28 +0x2e0
FAIL github.com/billalaashraf/simplebank/db/sqlc 0.698s
FAIL
What am i missing here.
Thanks for the great tutorial!
You currently use http.NewRequest
in your tests:
request, err := http.NewRequest(...)
require.NoError(t, err)
This could be replaced with httptest.NewRequest
which panics on error
request := httptest.NewRequest(...)
if len(roleApis) > 0 {
db := db.Orm.Debug().Model(&models.Api{}).
Select("system_api.id").
Joins("left join system_menu_api on system_menu_api.api = system_api.id")
for _, p := range roleApis {
db = db.Or("system_api.url = ? and system_api.method = ?", p[1], p[2])
}
err = db.Where("system_menu_api.menu = ?", menuId).Pluck("system_api.id", &apis).Error
if err != nil {
response.Error(c, err, response.GetApiError)
return
}
}
The dgrijalva/jwt-go
is no longer maintained, and the development has been moved the a new repo golang-jwt/jwt
.
However, the recent version of golang-jwt/jwt
which is v5 has introduced a new breaking changes in the way Claims
interface works.
While technically we can still use the v4, It would be very helpful if you can update this repo to support the v5 version.
//creating random user. ///test function
user,_:= randomUser(t)
n := 5
//creating a slice of index n
airlines := make([]db.Airline, n)
for i:=0; i<n; i++ {
airlines[i] = RandomAirline(t)
}
//query
type Query struct {
pageID int
pageSize int
}
test_cases := []struct{
name string
query Query
setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker)
buildStubs func(store *mockdb.MockStore)
checkResponse func(recorder *httptest.ResponseRecorder)
}{
{
name : "OK",
query: Query {
pageID: 1,
pageSize: n,
},
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationBearerKey, user.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) {
arg := db.ListAirlinesParams{
Limit: int32(n),
Offset: 0,
}
store.EXPECT().ListAirlines(gomock.Any(), gomock.Eq(arg)).Times(1).Return(airlines, nil)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
},
},
}
///live functionality
type listAirlinesRequest struct {
pageID int32 `form:"page_id" binding:"required,min=1"`
pageSize int32`form:"page_size" binding:"required,min=5,max=10"`
}
func (server *Server) GetAirlines(ctx *gin.Context){
var req listAirlinesRequest
if err := ctx.ShouldBindQuery(&req); err != nil{
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}
arg := db.ListAirlinesParams{
Limit: req.pageSize,
Offset: (req.pageID - 1) * req.pageSize,
}
airlines, err := server.store.ListAirlines(ctx, arg)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
ctx.JSON(http.StatusOK, airlines)
/////error
--- FAIL: TestListAirlines (0.06s)
--- FAIL: TestListAirlines/OK (0.00s)
/Applications/XAMPP/xamppfiles/htdocs/SPS-Web/api/airline.go:92: Unexpected call to *mockdb.MockStore.ListAirlines([0xc000124600 {0 0}]) at /Applications/XAMPP/xamppfiles/htdocs/SPS-Web/api/airline.go:92 because:
expected call at /Applications/XAMPP/xamppfiles/htdocs/SPS-Web/api/airline_test.go:142 doesn't match the argument at index 1.
Got: {0 0} (db.ListAirlinesParams)
Want: is equal to {5 0} (db.ListAirlinesParams)
/Applications/XAMPP/xamppfiles/htdocs/SPS-Web/api/controller.go:269: missing call(s) to *mockdb.MockStore.ListAirlines(is anything, is equal to {5 0} (db.ListAirlinesParams)) /Applications/XAMPP/xamppfiles/htdocs/SPS-Web/api/airline_test.go:142
/Applications/XAMPP/xamppfiles/htdocs/SPS-Web/api/controller.go:269: aborting test due to missing call(s)
FAIL
FAIL github.com/thinkIt-africa/SPS-Web/api 1.250s. ```
Eg. for updateAccount
API, How to bind uri
and json
for the same struct?
type updateAccountRequest struct {
ID int64 `uri:"id" binding:"required,min=1"`
Currency string `json:"currency" binding:"required,currency"`
}
func (server *Server) updateArticle(ctx *gin.Context) {
var req updateAccountRequest
if err := ctx.ShouldBindUri(&req); err != nil {
// send 400 Bad Request to the client
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}
if err := ctx.ShouldBindJSON(&req); err != nil {
// send 400 Bad Request to the client
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}
}
the code above does not work.
Does anyone know? Thanks in advance
Hi,
Thanks for the great tutorial.
In the following code, New
function expects DBTX
. However, sql.open
returns *sql.DB
and error
type therefore the code fails
conn, err := sql.Open(dbDriver, dbSource)
if err != nil {
log.Fatal("cannot connect to db: ", err)
}
testQueries = New(conn) // <<--- type fails here
The same issue will happen later
type Store struct {
*Queries
db *sql.DB
}
func NewStore(db *sql.DB) *Store {
return &Store{
db: db,
Queries: New(db), // <-- here
}
}
cannot use db (variable of type *sql.DB) as DBTX value in argument to New: *sql.DB does not implement DBTX (wrong type for method Exec)
have Exec(query string, args ...any) (sql.Result, error)
want Exec(context.Context, string, ...interface{}) (pgconn.CommandTag,
Thanks!
hi:How should the logic for logging out be implemented
Hello, firstly I would say thank you for the great tutorial in youtube.
I stumbled upon a problem where when I run make test
, it return error like this:
The problem are, when we executing the command for make test
, the test skipped func TestMain
so the database connection doesn't created hence, this happen.
Temporary solution is, well, make new sql connection for each function. But as I read from github.com/stretchr/testify/suite
, TestMain could be replaced by this.
First, thank you for your course and giving me a lot of help!
Without a license, the default copyright laws apply, meaning that code owner retain all rights to source code and no one may reproduce, distribute, or create derivative works from work.
Open source licenses grant permission to use, modify, and redistribute licensed software for any purpose, subject to conditions preserving the provenance and openness of the software.
How do you provide .env file content for the workflows without publicly including it in the repo?
Files in db/query have plural declaration of tables but when the sql.go files have singular structs of respective tables. Regenerate those files.
I run sqlc generate it encounter a error.
My CGO_ENABLED has long been set to 1, but sqlc still cannot run.
And i test my cgo ,it can run.
Here are the environment variables for my go and I use macos13.0
I've tried my best to fix it but still nothing works, hope someone can help me, I would be grateful
When I re-study Lecture 18, the unit test was only partially passed. I would like to ask if I have configured something wrong? Looking forward to your reply
The error is as follows:
=== RUN TestCreateUserAPI
=== RUN TestCreateUserAPI/OK
[GIN] 2023/11/21 - 22:35:15 | 400 | 77.282µs | | POST "/users"
/home/test/github.com/test/simplebank_reappear/api/user_test.go:55:
Error Trace: /home/test/github.com/test/simplebank_reappear/api/user_test.go:55
/home/test/github.com/test/simplebank_reappear/api/user_test.go:171
Error: Not equal:
expected: 200
actual : 400
Test: TestCreateUserAPI
/home/test/github.com/test/simplebank_reappear/api/controller.go:269: missing call(s) to *mockdb.MockStore.CreateUser(is anything, is equal to {yatlxo $2a$10$GlXhZH2JeqRG4ezTgC9uUeU9AEohAm4RY.TYGf5HeABoDvP.g.8/W slcpwu afqxbt@email} (db.CreateUserParams)) /home/test/github.com/test/simplebank_reappear/api/user_test.go:50
/home/test/github.com/test/simplebank_reappear/api/controller.go:269: aborting test due to missing call(s)
=== RUN TestCreateUserAPI/InternalError
[GIN] 2023/11/21 - 22:35:15 | 400 | 28.141µs | | POST "/users"
/home/test/github.com/test/simplebank_reappear/api/user_test.go:75:
Error Trace: /home/test/github.com/test/simplebank_reappear/api/user_test.go:75
/home/test/github.com/test/simplebank_reappear/api/user_test.go:171
Error: Not equal:
expected: 500
actual : 400
Test: TestCreateUserAPI
/home/test/github.com/test/simplebank_reappear/api/controller.go:269: missing call(s) to *mockdb.MockStore.CreateUser(is anything, is anything) /home/test/github.com/test/simplebank_reappear/api/user_test.go:70
/home/test/github.com/test/simplebank_reappear/api/controller.go:269: aborting test due to missing call(s)
=== RUN TestCreateUserAPI/DuplicateUsername
[GIN] 2023/11/21 - 22:35:15 | 400 | 15.003µs | | POST "/users"
/home/test/github.com/test/simplebank_reappear/api/user_test.go:93:
Error Trace: /home/test/github.com/test/simplebank_reappear/api/user_test.go:93
/home/test/github.com/test/simplebank_reappear/api/user_test.go:171
Error: Not equal:
expected: 403
actual : 400
Test: TestCreateUserAPI
/home/test/github.com/test/simplebank_reappear/api/controller.go:269: missing call(s) to *mockdb.MockStore.CreateUser(is anything, is anything) /home/test/github.com/test/simplebank_reappear/api/user_test.go:88
/home/test/github.com/test/simplebank_reappear/api/controller.go:269: aborting test due to missing call(s)
=== RUN TestCreateUserAPI/InvalidUsername
[GIN] 2023/11/21 - 22:35:15 | 400 | 16.933µs | | POST "/users"
=== RUN TestCreateUserAPI/InvalidEmail
[GIN] 2023/11/21 - 22:35:15 | 400 | 12.752µs | | POST "/users"
=== RUN TestCreateUserAPI/TooshortPassword
[GIN] 2023/11/21 - 22:35:15 | 400 | 14.628µs | | POST "/users"
--- FAIL: TestCreateUserAPI (0.14s)
--- FAIL: TestCreateUserAPI/OK (0.00s)
--- FAIL: TestCreateUserAPI/InternalError (0.00s)
--- FAIL: TestCreateUserAPI/DuplicateUsername (0.00s)
--- PASS: TestCreateUserAPI/InvalidUsername (0.00s)
--- PASS: TestCreateUserAPI/InvalidEmail (0.00s)
--- PASS: TestCreateUserAPI/TooshortPassword (0.00s)
FAIL
FAIL github.com/test/simplebank_reappear/api 0.143s
PS: I don’t have the file
api/controller.go
The user_test.go
is as follows
func TestCreateUserAPI(t *testing.T) {
user, password := randomUser(t)
hashedPassword, err := util.HashPassword(password)
require.NoError(t, err)
testCases := []struct {
name string
body gin.H
buildStubs func(store *mockdb.MockStore)
checkResponse func(recorder *httptest.ResponseRecorder)
}{
{
name: "OK",
body: gin.H{
"username": user.Username,
"password": password,
"full_name": user.FullName,
"email": user.Email,
},
buildStubs: func(store *mockdb.MockStore) {
arg := db.CreateUserParams{
Username: user.Username,
HashedPassword: hashedPassword,
FullName: user.FullName,
Email: user.Email,
}
store.EXPECT().
CreateUser(gomock.Any(), gomock.Eq(arg)).
Times(1).
Return(user, nil)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
fmt.Println("OK TEST", recorder.Body)
requireBodyMatchUser(t, recorder.Body, user)
},
},
{
name: "InternalError",
body: gin.H{
"username": user.Username,
"password": password,
"full_name": user.FullName,
"email": user.Email,
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(db.User{}, sql.ErrConnDone)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusInternalServerError, recorder.Code)
},
},
{
name: "DuplicateUsername",
body: gin.H{
"username": user.Username,
"password": password,
"full_name": user.FullName,
"email": user.Email,
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(db.User{}, &pq.Error{Code: "23505"})
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusForbidden, recorder.Code)
},
},
{
name: "InvalidUsername",
body: gin.H{
"username": "invalid-user#1",
"password": password,
"full_name": user.FullName,
"email": user.Email,
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(0)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusBadRequest, recorder.Code)
},
},
{
name: "InvalidEmail",
body: gin.H{
"username": user.Username,
"password": password,
"full_name": user.FullName,
"email": "invalid-email",
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(0)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusBadRequest, recorder.Code)
},
},
{
name: "TooshortPassword",
body: gin.H{
"username": user.Username,
"password": "123",
"full_name": user.FullName,
"email": user.Email,
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(0)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusBadRequest, recorder.Code)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockdb.NewMockStore(ctrl)
tc.buildStubs(store)
server := NewServer(store)
recorder := httptest.NewRecorder()
//Marshal body data to JSON
data, err := json.Marshal(tc.body)
require.NoError(t, err)
url := "/users"
request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
require.NoError(t, err)
server.router.ServeHTTP(recorder, request)
tc.checkResponse(recorder)
})
}
}
func randomUser(t *testing.T) (user db.User, password string) {
password = util.RandomString(6)
hashedPassword, err := util.HashPassword(password)
require.NoError(t, err)
user = db.User{
Username: util.RandomOwner(),
HashedPassword: hashedPassword,
FullName: util.RandomOwner(),
Email: util.RandomEmail(),
}
return
}
func requireBodyMatchUser(t *testing.T, body *bytes.Buffer, user db.User) {
data, err := io.ReadAll(body)
require.NoError(t, err)
var gotUser db.User
err = json.Unmarshal(data, &gotUser)
require.NoError(t, err)
require.Equal(t, user.Username, gotUser.Username)
require.Equal(t, user.FullName, gotUser.FullName)
require.Equal(t, user.Email, gotUser.Email)
require.Empty(t, gotUser.HashedPassword)
}
The user.go
is as follows
type createUserRequest struct {
Username string `json:"username" binding:"required,alphanum"`
Password string `json:"password" binding:"required,min=6"`
FullName string `json:"full_name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
type createUserResponse struct {
Username string `json:"username"`
FullName string `json:"full_name"`
Email string `json:"email"`
PasswordChangedAt time.Time `json:"password_changed_at"`
CreatedAt time.Time `json:"created_at"`
}
func (server *Server) createUser(ctx *gin.Context) {
var req createUserRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}
hashedPassword, err := util.HashPassword(req.Password)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
arg := db.CreateUserParams{
Username: req.Username,
HashedPassword: hashedPassword,
FullName: req.FullName,
Email: req.Email,
}
user, err := server.store.CreateUser(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() {
case "unique_violation":
ctx.JSON(http.StatusForbidden, errorResponse(err))
return
}
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
resp := createUserResponse{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
PasswordChangedAt: user.PasswordChangedAt,
CreatedAt: user.CreatedAt,
}
ctx.JSON(http.StatusOK, resp)
}
Thanks for this great class.
About lecture #25 (create docker-compose file), It is no longer used wait_for.sh for make order between services. and it isn't in docker document. It can implemented by condition and healthcheck simply. like below:
`version: '3.9'
services:
postgres:
image: postgres:14-alpine
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=simple_bank
ports:
- "5432:5432"
volumes:
- data-volume:/var/lib/postgresql/data
healthcheck:
test: "exit 0"
redis:
image: redis:7-alpine
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
- "9090:9090"
environment:
- DB_SOURCE=postgresql://root:secret@postgres:5432/simple_bank?sslmode=disable
- REDIS_ADDRESS=redis:6379
depends_on:
- postgres:
condition: service_healthy
- redis
volumes:
data-volume:`
Thanks again for this wonderful class.
Would you like add a feature: use swagger to show apis?
How can i execute raw sql query ? Can we create table using sqlc ?
I want to create a table dynamically through api .
Is there any way i can execute raw sql query with this series setup ?
Note: some components, such as, docker images and containers have different names from the course.
Thank you for your help!
Here are my files:
version: "3.9"
services:
postgres:
image: postgres:latest
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=simple_bank
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- DB_SOURCE=postgresql://root:secret@postgres:5432/simple_bank?sslmode=disable
depends_on:
- postgres
entrypoint: ["/app/wait-for.sh", "postgres:5432", "--", "/app/start.sh"]
command: ["/app/main"]
#Build Stage
FROM golang:1.19-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o main main.go
FROM alpine
WORKDIR /app
COPY --from=builder /app/main .
COPY /app/migrate ./migrate
COPY app.env .
COPY start.sh .
COPY wait-for.sh .
COPY db/migrations ./migration
EXPOSE 8080
CMD [ "/app/main" ]
ENTRYPOINT [ "/app/start.sh" ]
#!/bin/sh
set -e
echo "RUN DB migration"
/app/migrate -path /app/migration -database "$DB_SOURCE" -verbose up
echo "start the app"
exec ls -la
exec "$@"
Then I run docker compose down and delete my api image. However when I do docker compose up I get this error (in the picture), it might be something that I am not seeing... My files structure is in the second picture
Solved the issue, I was deleting the wrong image
Hello master, I have discovered a vulnerability in github.com/techschool/simplebank/util when using go mod tidy. There is no released version in this file, which prevents me from continuing to use commands
ListAccounts interface I'm not sure if I have the next page how do I improve this one
I get '2022-07-06T10:12:50.672517539Z', but I want '2022-07-06 10:12:50'
How to implement formatting in a generic way?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.