diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e733f06..398e3e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,8 @@ on: jobs: build-test: runs-on: ubuntu-latest + env: + DB_URL: postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable services: # Label used to access the service container postgres: @@ -44,9 +46,10 @@ jobs: run: go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.17 - name: Run migrations - env: - DB_URL: postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable run: make migrateup + - name: Create env configuration file + run: cp app.sample.env app.env + - name: Test run: go test -v -cover ./... diff --git a/app.sample.env b/app.sample.env index 4ba5c48..100f7f9 100644 --- a/app.sample.env +++ b/app.sample.env @@ -4,8 +4,8 @@ DB_DRIVER=postgres DB_USER=root DB_PASSWORD=secret DB_NAME=simple_bank -DB_PORT=5433 -DB_URL="postgresql://root:secret@localhost:5433/simple_bank?sslmode=disable" +DB_PORT=5432 +DB_URL="postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" DB_CONTAINER_NAME=postgres16 SERVER_ADDRESS=:8080 \ No newline at end of file diff --git a/pkg/api/account.go b/pkg/api/account.go index b7d9783..8314653 100644 --- a/pkg/api/account.go +++ b/pkg/api/account.go @@ -3,7 +3,6 @@ package api import ( "database/sql" "errors" - "fmt" "net/http" "github.com/aseerkt/go-simple-bank/pkg/db" @@ -35,8 +34,6 @@ func (s *Server) createAccount(c *gin.Context) { if err != nil { if pqError, ok := err.(*pq.Error); ok { - fmt.Println(err) - fmt.Println(pqError.Code.Name()) switch pqError.Code.Name() { case "foreign_key_violation", "unique_violation": handleForbidden(c, pqError) diff --git a/pkg/api/account_test.go b/pkg/api/account_test.go index 5a3c863..9fdb24b 100644 --- a/pkg/api/account_test.go +++ b/pkg/api/account_test.go @@ -1,42 +1,182 @@ package api import ( + "bytes" "database/sql" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" + "net/url" "testing" + "time" + "github.com/aseerkt/go-simple-bank/pkg/constants" "github.com/aseerkt/go-simple-bank/pkg/db" "github.com/aseerkt/go-simple-bank/pkg/mockdb" "github.com/brianvoe/gofakeit/v7" + "github.com/gin-gonic/gin" + "github.com/lib/pq" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) -func createRandomAccount() db.Account { - return db.Account{ +func createRandomAccount() (db.User, db.Account) { + user := db.User{ + Username: gofakeit.Username(), + FullName: gofakeit.Name(), + Email: gofakeit.Email(), + } + + return user, db.Account{ ID: gofakeit.Int64(), - Owner: gofakeit.Name(), + Owner: user.Username, Balance: gofakeit.Int64(), - Currency: gofakeit.CurrencyShort(), + Currency: gofakeit.RandomMapKey(constants.Currency).(string), + } +} + +func getAuthMiddleware(username string) func(t *testing.T, s *Server, r *http.Request) { + return func(t *testing.T, s *Server, r *http.Request) { + token, err := s.tokenMaker.CreateToken(username, 15*time.Minute) + require.NoError(t, err) + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + } +} + +func TestCreateAccountAPI(t *testing.T) { + + user, account := createRandomAccount() + + setupAuth := getAuthMiddleware(user.Username) + + testCases := []struct { + name string + body gin.H + setupAuth func(t *testing.T, s *Server, r *http.Request) + buildStub func(store *mockdb.MockStore) + checkResponse func(t *testing.T, r *httptest.ResponseRecorder) + }{ + { + name: "Ok", + setupAuth: setupAuth, + body: gin.H{ + "currency": "INR", + }, + buildStub: func(store *mockdb.MockStore) { + arg := db.CreateAccountParams{ + Owner: user.Username, + Currency: "INR", + Balance: 0, + } + store.EXPECT().CreateAccount(gomock.Any(), gomock.Eq(arg)).Times(1).Return(account, nil) + }, + checkResponse: func(t *testing.T, r *httptest.ResponseRecorder) { + require.Equal(t, http.StatusCreated, r.Code) + var createdAccount db.Account + err := json.Unmarshal(r.Body.Bytes(), &createdAccount) + require.NoError(t, err) + require.Equal(t, createdAccount, account) + }, + }, + { + name: "InvalidCurrency", + setupAuth: setupAuth, + body: gin.H{ + "currency": "NONE", + }, + buildStub: func(store *mockdb.MockStore) { + store.EXPECT().CreateAccount(gomock.Any(), gomock.Any()).Times(0) + }, + checkResponse: func(t *testing.T, r *httptest.ResponseRecorder) { + require.Equal(t, http.StatusBadRequest, r.Code) + }, + }, + { + name: "UniqueViolation", + setupAuth: setupAuth, + body: gin.H{ + "currency": "INR", + }, + buildStub: func(store *mockdb.MockStore) { + arg := db.CreateAccountParams{ + Owner: user.Username, + Currency: "INR", + Balance: 0, + } + err := &pq.Error{ + Code: "23505", + } + store.EXPECT().CreateAccount(gomock.Any(), gomock.Eq(arg)).Times(1).Return(db.Account{}, err) + }, + checkResponse: func(t *testing.T, r *httptest.ResponseRecorder) { + require.Equal(t, http.StatusForbidden, r.Code) + + }, + }, + { + name: "InternalError", + setupAuth: setupAuth, + body: gin.H{ + "currency": "INR", + }, + buildStub: func(store *mockdb.MockStore) { + arg := db.CreateAccountParams{ + Owner: user.Username, + Currency: "INR", + Balance: 0, + } + store.EXPECT().CreateAccount(gomock.Any(), gomock.Eq(arg)).Times(1).Return(db.Account{}, sql.ErrConnDone) + }, + checkResponse: func(t *testing.T, r *httptest.ResponseRecorder) { + require.Equal(t, http.StatusInternalServerError, r.Code) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mockdb.NewMockStore(ctrl) + tc.buildStub(store) + + server := newTestServer(store) + server.LoadRoutes() + + body, err := json.Marshal(tc.body) + require.NoError(t, err) + + request, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(body)) + require.NoError(t, err) + + tc.setupAuth(t, server, request) + + recorder := httptest.NewRecorder() + server.router.ServeHTTP(recorder, request) + tc.checkResponse(t, recorder) + }) } } func TestGetAccountAPI(t *testing.T) { - account := createRandomAccount() + user, account := createRandomAccount() + + setupAuth := getAuthMiddleware(user.Username) testCases := []struct { name string accountID int64 + setupAuth func(t *testing.T, s *Server, r *http.Request) buildStub func(store *mockdb.MockStore) checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder) }{ { name: "Ok", accountID: account.ID, + setupAuth: setupAuth, buildStub: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(account, nil) }, @@ -58,6 +198,7 @@ func TestGetAccountAPI(t *testing.T) { { name: "NotFound", accountID: account.ID, + setupAuth: setupAuth, buildStub: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(db.Account{}, sql.ErrNoRows) }, @@ -68,6 +209,7 @@ func TestGetAccountAPI(t *testing.T) { { name: "InternalError", accountID: account.ID, + setupAuth: setupAuth, buildStub: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(db.Account{}, sql.ErrConnDone) }, @@ -78,6 +220,7 @@ func TestGetAccountAPI(t *testing.T) { { name: "InvalidID", accountID: 0, + setupAuth: setupAuth, buildStub: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(0)).Times(0) }, @@ -85,6 +228,17 @@ func TestGetAccountAPI(t *testing.T) { require.Equal(t, http.StatusBadRequest, recorder.Code) }, }, + { + name: "AccountMismatch", + accountID: account.ID, + setupAuth: getAuthMiddleware("alfred"), + buildStub: func(store *mockdb.MockStore) { + store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account.ID)).Times(1).Return(account, nil) + }, + checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, } for i := range testCases { @@ -110,10 +264,124 @@ func TestGetAccountAPI(t *testing.T) { require.NoError(t, err) + tc.setupAuth(t, server, request) + server.router.ServeHTTP(recorder, request) tc.checkResponse(t, recorder) }) } +} + +func TestListAccountsAPI(t *testing.T) { + + var username string + var accounts []db.Account + + for range 5 { + user, account := createRandomAccount() + if username == "" { + username = user.Username + } else { + account.Owner = username + } + accounts = append(accounts, account) + } + + setPaginationQuery := func(r *http.Request, pageID int, pageSize int) { + var query url.Values = make(url.Values) + + query.Add("page_id", fmt.Sprint(pageID)) + query.Add("page_size", fmt.Sprint(pageSize)) + + r.URL.RawQuery = query.Encode() + } + + testCases := []struct { + name string + setQuery func(r *http.Request) + buildStub func(store *mockdb.MockStore) + checkResponse func(t *testing.T, r *httptest.ResponseRecorder) + }{ + { + name: "Ok", + setQuery: func(r *http.Request) { + setPaginationQuery(r, 1, 5) + }, + buildStub: func(store *mockdb.MockStore) { + arg := db.ListAccountsParams{ + Owner: username, + Offset: 0, + Limit: 5, + } + store.EXPECT().ListAccounts(gomock.Any(), gomock.Eq(arg)).Times(1).Return(accounts, nil) + }, + checkResponse: func(t *testing.T, r *httptest.ResponseRecorder) { + require.Equal(t, http.StatusOK, r.Code) + var listedAccounts []db.Account + err := json.Unmarshal(r.Body.Bytes(), &listedAccounts) + require.NoError(t, err) + require.Equal(t, listedAccounts, accounts) + + }, + }, + { + name: "InvalidQuery", + setQuery: func(r *http.Request) { + setPaginationQuery(r, 0, 25) + }, + buildStub: func(store *mockdb.MockStore) { + store.EXPECT().ListAccounts(gomock.Any(), gomock.Any()).Times(0) + }, + checkResponse: func(t *testing.T, r *httptest.ResponseRecorder) { + require.Equal(t, http.StatusBadRequest, r.Code) + }, + }, + { + name: "InternalServerError", + setQuery: func(r *http.Request) { + setPaginationQuery(r, 1, 5) + }, + buildStub: func(store *mockdb.MockStore) { + arg := db.ListAccountsParams{ + Owner: username, + Offset: 0, + Limit: 5, + } + store.EXPECT().ListAccounts(gomock.Any(), gomock.Eq(arg)).Times(1).Return([]db.Account{}, sql.ErrConnDone) + }, + checkResponse: func(t *testing.T, r *httptest.ResponseRecorder) { + require.Equal(t, http.StatusInternalServerError, r.Code) + }, + }, + } + + setupAuth := getAuthMiddleware(username) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mockdb.NewMockStore(ctrl) + tc.buildStub(store) + + server := newTestServer(store) + server.LoadRoutes() + + request, err := http.NewRequest(http.MethodGet, "/accounts", nil) + + require.NoError(t, err) + + setupAuth(t, server, request) + + tc.setQuery(request) + + recorder := httptest.NewRecorder() + server.router.ServeHTTP(recorder, request) + + tc.checkResponse(t, recorder) + }) + } } diff --git a/pkg/api/user.go b/pkg/api/user.go index 6d03f89..09f4836 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -80,7 +80,6 @@ type loginUserPayload struct { func (s *Server) loginUser(c *gin.Context) { var payload loginUserPayload if err := c.ShouldBindJSON(&payload); err != nil { - fmt.Println("body json is invalid") handleBadRequest(c, err) return } diff --git a/pkg/constants/currency.go b/pkg/constants/currency.go new file mode 100644 index 0000000..306ac05 --- /dev/null +++ b/pkg/constants/currency.go @@ -0,0 +1,8 @@ +package constants + +var Currency = map[string]string{ + "USD": "USD", + "EUR": "EUR", + "CAD": "CAD", + "INR": "INR", +} diff --git a/pkg/token/jwt_maker_test.go b/pkg/token/jwt_maker_test.go index d251860..c18b3be 100644 --- a/pkg/token/jwt_maker_test.go +++ b/pkg/token/jwt_maker_test.go @@ -101,19 +101,6 @@ func TestVerifyToken(t *testing.T) { require.ErrorIs(t, err, jwt.ErrTokenUnverifiable) }, }, - { - name: "InvalidClaims", - createToken: func() string { - jwtToken := jwt.New(jwt.SigningMethodHS256) - token, err := jwtToken.SignedString([]byte(secretKey)) - require.NoError(t, err) - return token - }, - checkResults: func(p *Payload, err error) { - require.Empty(t, p) - require.Error(t, err) - }, - }, } for _, tc := range testCases { diff --git a/pkg/utils/config.go b/pkg/utils/config.go index 4ab40b8..28adc3c 100644 --- a/pkg/utils/config.go +++ b/pkg/utils/config.go @@ -1,7 +1,6 @@ package utils import ( - "fmt" "log" "github.com/spf13/viper" @@ -30,7 +29,5 @@ func LoadConfig(path string) Config { log.Fatal("unable to unmarshal config: ", err) } - fmt.Println(config) - return config } diff --git a/pkg/utils/constants.go b/pkg/utils/constants.go index aed0991..6799df0 100644 --- a/pkg/utils/constants.go +++ b/pkg/utils/constants.go @@ -6,5 +6,3 @@ const ( CAD = "CAD" INR = "INR" ) - - diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go index 37b1826..1e9d098 100644 --- a/pkg/utils/helpers.go +++ b/pkg/utils/helpers.go @@ -1,9 +1,8 @@ package utils +import "github.com/aseerkt/go-simple-bank/pkg/constants" + func IsSupportedCurrency(currency string) bool { - switch currency { - case USD, EUR, CAD, INR: - return true - } - return false + _, ok := constants.Currency[currency] + return ok }