From f1934f8914b3c452c7cbb0a9318a8633c1495667 Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Sun, 25 Aug 2024 01:39:26 +0300 Subject: [PATCH 01/10] test: add integration tests for signup endpoint --- Makefile | 9 ++-- internal/models/schema.go | 9 ++++ tests/auth/signup_test.go | 95 +++++++++++++++++++++++++++++++++++++++ tests/main_test.go | 18 ++++++++ tests/setup/setup.go | 39 ++++++++++++++++ 5 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 tests/auth/signup_test.go create mode 100644 tests/main_test.go create mode 100644 tests/setup/setup.go diff --git a/Makefile b/Makefile index 633efda..bd4298d 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ APP_NAME := Appcrons CMD_DIR := ./cmd BIN_DIR := ./bin -TEST_DIR := ./test +TEST_DIR := ./tests # Ensure bin directory exists $(BIN_DIR): @@ -16,13 +16,14 @@ all: install run .PHONY: run run: @echo "Starting development server..." - @go run $(CMD_DIR) + @ @GO_ENV=development go run $(CMD_DIR) # Run tests .PHONY: test test: - @echo "Running tests..." - @go test $(TEST_DIR) -v + @echo "Running tests with GO_ENV=testing..." + @GO_ENV=testing go test -v $(TEST_DIR)/... + # Install dependencies .PHONY: install diff --git a/internal/models/schema.go b/internal/models/schema.go index 8878a03..828063b 100644 --- a/internal/models/schema.go +++ b/internal/models/schema.go @@ -10,6 +10,7 @@ import ( ) var db = config.Db() +var DB = db var redisClient = config.RedisClient() var ctx = context.Background() @@ -21,6 +22,14 @@ func DBAutoMigrate() { log.Println("Auto Migration successful") } +func DBDropTables() { + err := db.Migrator().DropTable(&User{}, &App{}, &Request{}, &RequestTime{}, &Feedback{}) + if err != nil { + log.Fatal("Failed to drop tables", err) + } + log.Println("Dropped all tables") +} + type User struct { ID string `gorm:"column:id;type:uuid;primaryKey" json:"id"` Name string `gorm:"column:name;not null;index" json:"name"` diff --git a/tests/auth/signup_test.go b/tests/auth/signup_test.go new file mode 100644 index 0000000..550f4a4 --- /dev/null +++ b/tests/auth/signup_test.go @@ -0,0 +1,95 @@ +package auth_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Tibz-Dankan/keep-active/tests/setup" +) + +func TestMissingSignUpFields(t *testing.T) { + setup.ClearAllTables() + + var label string + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + + label = "Expects 400 with missing username" + payload = []byte(`{"name":"","email":"email@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) + + label = "Expects 400 with missing email" + payload = []byte(`{"name":"username","email":"","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) + + label = "Expects 400 with missing password" + payload = []byte(`{"name":"","email":"user@gmail.com","password":""}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestExistingUser(t *testing.T) { + setup.ClearAllTables() + + var label string + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + + label = "Expects 400 when user already exists" + payload = []byte(`{"name":"username","email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) + _ = setup.ExecuteRequest(req) + + payload = []byte(`{"name":"username","email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestSuccessfulSignup(t *testing.T) { + setup.ClearAllTables() + + var label string + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + label = "Expects 201 on successful signup" + payload = []byte(`{"name":"username","email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusCreated, response.Code) + + json.Unmarshal(response.Body.Bytes(), &body) + + label = "Expects accessToken on successful signup" + token, found := body["accessToken"] + if !found { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + + accessToken, ok := token.(string) + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", accessToken) + } + if accessToken == "" { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got an empty string") + } else { + fmt.Printf("=== PASS: %s\n", label) + } +} diff --git a/tests/main_test.go b/tests/main_test.go new file mode 100644 index 0000000..61dec00 --- /dev/null +++ b/tests/main_test.go @@ -0,0 +1,18 @@ +package tests + +import ( + "os" + "testing" + + "github.com/Tibz-Dankan/keep-active/internal/models" +) + +func TestMain(m *testing.M) { + models.DBAutoMigrate() + + code := m.Run() + + models.DBDropTables() + + os.Exit(code) +} diff --git a/tests/setup/setup.go b/tests/setup/setup.go new file mode 100644 index 0000000..63a0cec --- /dev/null +++ b/tests/setup/setup.go @@ -0,0 +1,39 @@ +package setup + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Tibz-Dankan/keep-active/internal/models" + "github.com/Tibz-Dankan/keep-active/internal/routes" +) + +var db = models.DB + +func ClearAllTables() { + tables := []string{"requests", "request_times", "apps", "feedbacks", "users"} + + for _, table := range tables { + statement := fmt.Sprintf("DELETE FROM %s ;", table) + db.Exec(statement) + } +} + +func ExecuteRequest(req *http.Request) *httptest.ResponseRecorder { + rr := httptest.NewRecorder() + r := routes.AppRouter() + r.ServeHTTP(rr, req) + + return rr +} + +func CheckResponseCode(t *testing.T, label string, expected, actual int) { + if expected != actual { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expected response code %d. Got %d\n", expected, actual) + } else { + fmt.Printf("=== PASS: %s\n", label) + } +} From 31003c03d65113d792e7cbe21bbb734d9a7163e3 Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Sun, 25 Aug 2024 01:51:57 +0300 Subject: [PATCH 02/10] clean: remove unnecesary commands in the Makefile --- Makefile | 46 +--------------------------------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/Makefile b/Makefile index bd4298d..044c21a 100644 --- a/Makefile +++ b/Makefile @@ -1,64 +1,20 @@ -# Variables -APP_NAME := Appcrons CMD_DIR := ./cmd -BIN_DIR := ./bin TEST_DIR := ./tests -# Ensure bin directory exists -$(BIN_DIR): - @mkdir -p $(BIN_DIR) - -# Default target -.PHONY: all -all: install run - -# Run the development server .PHONY: run run: @echo "Starting development server..." - @ @GO_ENV=development go run $(CMD_DIR) + @GO_ENV=development go run $(CMD_DIR) -# Run tests .PHONY: test test: @echo "Running tests with GO_ENV=testing..." @GO_ENV=testing go test -v $(TEST_DIR)/... -# Install dependencies .PHONY: install install: @echo "Installing dependencies..." @go mod tidy @go mod download -# Clean up -.PHONY: clean -clean: - @echo "Cleaning up..." - @go clean - @rm -rf $(BIN_DIR) - -# Format code -.PHONY: fmt -fmt: - @echo "Formatting code..." - @go fmt $(CMD_DIR) $(TEST_DIR) - -# Build the application -.PHONY: build -build: $(BIN_DIR) - @echo "Building application..." - @go build -o $(BIN_DIR)/$(APP_NAME) $(CMD_DIR) - -# Run application in the background -.PHONY: start -start: $(BIN_DIR) - @echo "Starting application in the background..." - @nohup $(BIN_DIR)/$(APP_NAME) & - -# Stop the application -.PHONY: stop -stop: - @echo "Stopping application..." - @pkill -f "$(BIN_DIR)/$(APP_NAME)" From 1dad305e4d43c44f25fafb068d66237b502bf7e2 Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Sun, 25 Aug 2024 11:17:14 +0300 Subject: [PATCH 03/10] fix: remove string matching for 'invalid character' among jwt errors since its generic --- internal/services/error.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/services/error.go b/internal/services/error.go index dadb992..04b67c3 100644 --- a/internal/services/error.go +++ b/internal/services/error.go @@ -127,12 +127,11 @@ func (jwt *JWTError) Send(w http.ResponseWriter) { } func (jwt *JWTError) invalid() bool { - hasInvalidCharacter := strings.Contains(jwt.err, "invalid character") isIllegal := strings.Contains(jwt.err, "illegal base64 data") invalidSignature := jwt.err == "signature is invalid" invalidSigningMethod := jwt.err == "signing method (alg) is unspecified" - isInvalidJWT := hasInvalidCharacter || isIllegal || invalidSignature || invalidSigningMethod + isInvalidJWT := isIllegal || invalidSignature || invalidSigningMethod return isInvalidJWT } From b3e3a656e0ace72876ecd43ed0e016382c1be7d8 Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Sun, 25 Aug 2024 11:23:38 +0300 Subject: [PATCH 04/10] test: add integration tests for signin endpoint --- internal/routes/auth/signin.go | 2 +- internal/routes/auth/signup.go | 2 +- tests/auth/signin_test.go | 74 ++++++++++++++++++++++++++++++++++ tests/auth/signup_test.go | 2 +- tests/main_test.go | 3 +- 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 tests/auth/signin_test.go diff --git a/internal/routes/auth/signin.go b/internal/routes/auth/signin.go index 3c9f518..935e7c2 100644 --- a/internal/routes/auth/signin.go +++ b/internal/routes/auth/signin.go @@ -16,7 +16,7 @@ func signIn(w http.ResponseWriter, r *http.Request) { err := json.NewDecoder(r.Body).Decode(&user) if err != nil { - services.AppError(err.Error(), 400, w) + services.AppError(err.Error(), 500, w) return } diff --git a/internal/routes/auth/signup.go b/internal/routes/auth/signup.go index bf6514b..a14b126 100644 --- a/internal/routes/auth/signup.go +++ b/internal/routes/auth/signup.go @@ -15,7 +15,7 @@ func signUp(w http.ResponseWriter, r *http.Request) { err := json.NewDecoder(r.Body).Decode(&user) if err != nil { - services.AppError(err.Error(), 400, w) + services.AppError(err.Error(), 500, w) return } diff --git a/tests/auth/signin_test.go b/tests/auth/signin_test.go new file mode 100644 index 0000000..e307cdf --- /dev/null +++ b/tests/auth/signin_test.go @@ -0,0 +1,74 @@ +package auth_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Tibz-Dankan/keep-active/tests/setup" +) + +func TestInvalidSignInCredentials(t *testing.T) { + setup.ClearAllTables() + + var label string + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + + label = "Expects 400 with missing/invalid email" + payload = []byte(`{"email":"","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) + + label = "Expects 400 with missing/invalid password" + payload = []byte(`{"email":"user@gmail.com","password":""}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestSuccessfulSignIn(t *testing.T) { + setup.ClearAllTables() + + var label string + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + label = "Expects 200 on successful signup" + payload = []byte(`{"name":"username","email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) + _ = setup.ExecuteRequest(req) + + payload = []byte(`{"email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusOK, response.Code) + + json.Unmarshal(response.Body.Bytes(), &body) + + label = "Expects accessToken on successful signin" + token, found := body["accessToken"] + if !found { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + + accessToken, ok := token.(string) + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", accessToken) + } + if accessToken == "" { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got an empty string") + } else { + fmt.Printf("=== PASS: %s\n", label) + } +} diff --git a/tests/auth/signup_test.go b/tests/auth/signup_test.go index 550f4a4..6ee49b7 100644 --- a/tests/auth/signup_test.go +++ b/tests/auth/signup_test.go @@ -20,7 +20,7 @@ func TestMissingSignUpFields(t *testing.T) { var response *httptest.ResponseRecorder label = "Expects 400 with missing username" - payload = []byte(`{"name":"","email":"email@gmail.com","password":"password"}`) + payload = []byte(`{"name":"","email":"user@gmail.com","password":"password"}`) req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) response = setup.ExecuteRequest(req) setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) diff --git a/tests/main_test.go b/tests/main_test.go index 61dec00..392a042 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/Tibz-Dankan/keep-active/internal/models" + "github.com/Tibz-Dankan/keep-active/tests/setup" ) func TestMain(m *testing.M) { @@ -12,7 +13,7 @@ func TestMain(m *testing.M) { code := m.Run() - models.DBDropTables() + setup.ClearAllTables() os.Exit(code) } From 4d28c543d34e3bbcc35570892789b3753308cc31 Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Sun, 25 Aug 2024 12:10:47 +0300 Subject: [PATCH 05/10] test: add integration tests for forgotPassword endpoint --- internal/routes/auth/forgotPassword.go | 2 +- internal/services/email.go | 3 ++ tests/auth/forgotPassword_test.go | 48 ++++++++++++++++++++++++++ tests/auth/signup_test.go | 2 +- 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tests/auth/forgotPassword_test.go diff --git a/internal/routes/auth/forgotPassword.go b/internal/routes/auth/forgotPassword.go index 575e1e6..4f29648 100644 --- a/internal/routes/auth/forgotPassword.go +++ b/internal/routes/auth/forgotPassword.go @@ -16,7 +16,7 @@ func forgotPassword(w http.ResponseWriter, r *http.Request) { err := json.NewDecoder(r.Body).Decode(&user) if err != nil { - services.AppError(err.Error(), 400, w) + services.AppError(err.Error(), 500, w) return } diff --git a/internal/services/email.go b/internal/services/email.go index 795f162..8788408 100644 --- a/internal/services/email.go +++ b/internal/services/email.go @@ -92,6 +92,9 @@ func (u *Email) send(html, subject string) error { } func (e *Email) SendResetPassword(name, URL, subject string) error { + if os.Getenv("GO_ENV") == "testing" || os.Getenv("GO_ENV") == "staging" { + return nil + } data := struct { Subject string Name string diff --git a/tests/auth/forgotPassword_test.go b/tests/auth/forgotPassword_test.go new file mode 100644 index 0000000..65dcd09 --- /dev/null +++ b/tests/auth/forgotPassword_test.go @@ -0,0 +1,48 @@ +package auth_test + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Tibz-Dankan/keep-active/tests/setup" +) + +func TestNonExistingForgotPasswordUser(t *testing.T) { + setup.ClearAllTables() + + var label string + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + + label = "Expects 400 for non existing user" + payload = []byte(`{"name":"username","email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) + _ = setup.ExecuteRequest(req) + + payload = []byte(`{"email":"user20@gmail.com"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/forgot-password", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestSuccessfulForgotPasswordInitialization(t *testing.T) { + setup.ClearAllTables() + + var label string + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + + label = "Expects 400 for non existing user" + payload = []byte(`{"name":"username","email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signup", bytes.NewBuffer(payload)) + _ = setup.ExecuteRequest(req) + + payload = []byte(`{"email":"user@gmail.com"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/forgot-password", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusOK, response.Code) +} diff --git a/tests/auth/signup_test.go b/tests/auth/signup_test.go index 6ee49b7..d898b0a 100644 --- a/tests/auth/signup_test.go +++ b/tests/auth/signup_test.go @@ -38,7 +38,7 @@ func TestMissingSignUpFields(t *testing.T) { setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) } -func TestExistingUser(t *testing.T) { +func TestExistingSignUpUser(t *testing.T) { setup.ClearAllTables() var label string From 5be62b1949a0ab6608240f50682d7326a488d0de Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Sun, 25 Aug 2024 13:32:24 +0300 Subject: [PATCH 06/10] test: add integration tests for resetPassword endpoint --- internal/routes/auth/resetPassword.go | 2 +- tests/auth/resetPassword_test.go | 161 ++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 tests/auth/resetPassword_test.go diff --git a/internal/routes/auth/resetPassword.go b/internal/routes/auth/resetPassword.go index eecb147..bba80a9 100644 --- a/internal/routes/auth/resetPassword.go +++ b/internal/routes/auth/resetPassword.go @@ -16,7 +16,7 @@ func resetPassword(w http.ResponseWriter, r *http.Request) { err := json.NewDecoder(r.Body).Decode(&user) if err != nil { - services.AppError(err.Error(), 400, w) + services.AppError(err.Error(), 500, w) return } diff --git a/tests/auth/resetPassword_test.go b/tests/auth/resetPassword_test.go new file mode 100644 index 0000000..db64e6c --- /dev/null +++ b/tests/auth/resetPassword_test.go @@ -0,0 +1,161 @@ +package auth_test + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/Tibz-Dankan/keep-active/internal/models" + "github.com/Tibz-Dankan/keep-active/tests/setup" + "github.com/google/uuid" +) + +func TestMissingResetPasswordToken(t *testing.T) { + setup.ClearAllTables() + + var label string + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + + payload = []byte(`{"password":"password"}`) + req, _ = http.NewRequest("PATCH", "/api/v1/auth/reset-password/''", bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestMissingNewResetPassword(t *testing.T) { + setup.ClearAllTables() + + var label string = "Expects 400 with expired password reset token" + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + + user := models.User{Name: "username", Email: "user@gmail.com", Password: "password"} + + userId, err := user.Create(user) + if err != nil { + log.Println(err) + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + user.ID = userId + + resetToken, err := user.CreatePasswordResetToken() + if err != nil { + log.Println(err) + return + } + + payload = []byte(`{"password":""}`) + path := fmt.Sprintf("/api/v1/auth/reset-password/%s", resetToken) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestExpiredPasswordResetToken(t *testing.T) { + setup.ClearAllTables() + + var label string + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + + db := models.DB + user := models.User{Name: "username", Email: "user@gmail.com", Password: "password"} + + userId, err := user.Create(user) + if err != nil { + log.Println(err) + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + user.ID = userId + + resetToken := uuid.NewString() + hashedToken := sha256.New() + hashedToken.Write([]byte(resetToken)) + hashedTokenByteSlice := hashedToken.Sum(nil) + hashedTokenString := hex.EncodeToString(hashedTokenByteSlice) + + user.PasswordResetToken = hashedTokenString + user.PasswordResetExpiresAt = time.Now().Add(-20 * time.Minute) + db.Save(&user) + + label = "Expects 400 with expired password reset token" + payload = []byte(`{"password":"newPassword"}`) + path := fmt.Sprintf("/api/v1/auth/reset-password/%s", resetToken) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestSuccessfulPasswordReset(t *testing.T) { + setup.ClearAllTables() + + var label string = "Expects 200 on successful password reset" + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + db := models.DB + user := models.User{Name: "username", Email: "user@gmail.com", Password: "password"} + + userId, err := user.Create(user) + if err != nil { + log.Println(err) + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + user.ID = userId + + resetToken := uuid.NewString() + hashedToken := sha256.New() + hashedToken.Write([]byte(resetToken)) + hashedTokenByteSlice := hashedToken.Sum(nil) + hashedTokenString := hex.EncodeToString(hashedTokenByteSlice) + + user.PasswordResetToken = hashedTokenString + user.PasswordResetExpiresAt = time.Now().Add(20 * time.Minute) + db.Save(&user) + + payload = []byte(`{"password":"newPassword"}`) + path := fmt.Sprintf("/api/v1/auth/reset-password/%s", resetToken) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusOK, response.Code) + + json.Unmarshal(response.Body.Bytes(), &body) + + label = "Expects accessToken on successful password reset" + token, found := body["accessToken"] + if !found { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + + accessToken, ok := token.(string) + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", accessToken) + } + if accessToken == "" { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got an empty string") + } else { + fmt.Printf("=== PASS: %s\n", label) + } +} From a138cab663d289d85d360359435588889e3def97 Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Sun, 25 Aug 2024 17:22:46 +0300 Subject: [PATCH 07/10] fix: remove rate limiting for tests --- internal/middlewares/rateLimiter.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/middlewares/rateLimiter.go b/internal/middlewares/rateLimiter.go index 9abf872..47ef30d 100644 --- a/internal/middlewares/rateLimiter.go +++ b/internal/middlewares/rateLimiter.go @@ -3,6 +3,7 @@ package middlewares import ( "log" "net/http" + "os" "sync" "time" @@ -50,6 +51,11 @@ func (rl *RateLimiter) AllowRequest(clientIp string) bool { func RateLimit(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + if os.Getenv("GO_ENV") == "testing" || os.Getenv("GO_ENV") == "staging" { + next.ServeHTTP(w, r) + return + } clientIP := r.Header.Get("X-Forwarded-For") if clientIP == "" { clientIP = r.Header.Get("X-Real-IP") From 62a78eaf5cb2a51409cb81479ecee5fdd8f6c075 Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Sun, 25 Aug 2024 22:26:21 +0300 Subject: [PATCH 08/10] test: add integration tests for update user details endpoint --- internal/models/permissions.go | 10 +- internal/models/schema.go | 2 +- internal/models/user.go | 3 +- internal/routes/auth/resetPassword.go | 13 +- internal/routes/auth/signin.go | 13 +- internal/routes/auth/signup.go | 18 ++- internal/routes/feedback/postFeedback.go | 15 ++- internal/routes/request/postRequestTime.go | 15 ++- tests/auth/updateUserDetails_test.go | 150 +++++++++++++++++++++ tests/main_test.go | 9 ++ 10 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 tests/auth/updateUserDetails_test.go diff --git a/internal/models/permissions.go b/internal/models/permissions.go index 0285f15..92d0ce8 100644 --- a/internal/models/permissions.go +++ b/internal/models/permissions.go @@ -2,6 +2,7 @@ package models import ( "encoding/json" + "errors" "log" "time" ) @@ -45,17 +46,20 @@ func (p *Permissions) Set(userId string) error { } if savedUser.ID == "" { - log.Println("User does not exist!") - return nil //TODO: To return custom error message + return errors.New("User does not exist") } userPermissions.Role = savedUser.Role + if userPermissions.Role == "user" { + userPermissions.Permissions = []string{"READ", "WRITE", "EDIT", "DELETE"} + } + if userPermissions.Role == "client" { userPermissions.Permissions = []string{"READ", "WRITE", "EDIT", "DELETE"} } - if userPermissions.Role == "admin" { + if userPermissions.Role == "sys_admin" { userPermissions.Permissions = []string{"READ"} } diff --git a/internal/models/schema.go b/internal/models/schema.go index 828063b..19b20e2 100644 --- a/internal/models/schema.go +++ b/internal/models/schema.go @@ -37,7 +37,7 @@ type User struct { Password string `gorm:"column:password;not null" json:"password"` PasswordResetToken string `gorm:"column:passwordResetToken;index" json:"passwordResetToken"` PasswordResetExpiresAt time.Time `gorm:"column:passwordResetExpiresAt;index" json:"passwordResetExpiresAt"` - Role string `gorm:"column:role;default:'admin';not null" json:"role"` + Role string `gorm:"column:role;default:'user';not null" json:"role"` App []App `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"apps"` Feedback []Feedback `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"feedbacks"` CreatedAt time.Time `gorm:"column:createdAt" json:"createdAt"` diff --git a/internal/models/user.go b/internal/models/user.go index e4690e9..fca660b 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -106,8 +106,7 @@ func (u *User) HashPassword(plainTextPassword string) (string, error) { } func (u *User) ValidRole(role string) bool { - roles := []string{"admin", "client", "staff"} - // TODO: TO change roles to "user and sys_admin" + roles := []string{"user", "sys_admin"} for _, r := range roles { if r == role { diff --git a/internal/routes/auth/resetPassword.go b/internal/routes/auth/resetPassword.go index bba80a9..069328d 100644 --- a/internal/routes/auth/resetPassword.go +++ b/internal/routes/auth/resetPassword.go @@ -2,7 +2,9 @@ package auth import ( "encoding/json" + "log" "net/http" + "os" "github.com/Tibz-Dankan/keep-active/internal/events" "github.com/Tibz-Dankan/keep-active/internal/models" @@ -51,6 +53,15 @@ func resetPassword(w http.ResponseWriter, r *http.Request) { return } + if os.Getenv("GO_ENV") == "testing" || os.Getenv("GO_ENV") == "staging" { + permission := models.Permissions{} + if err := permission.Set(user.ID); err != nil { + log.Println("Error setting permissions:", err) + } + } else { + events.EB.Publish("permissions", user) + } + userMap := map[string]interface{}{ "id": user.ID, "name": user.Name, @@ -67,8 +78,6 @@ func resetPassword(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) - - events.EB.Publish("permissions", user) } func ResetPasswordRoute(router *mux.Router) { diff --git a/internal/routes/auth/signin.go b/internal/routes/auth/signin.go index 935e7c2..99db8c5 100644 --- a/internal/routes/auth/signin.go +++ b/internal/routes/auth/signin.go @@ -2,7 +2,9 @@ package auth import ( "encoding/json" + "log" "net/http" + "os" "github.com/Tibz-Dankan/keep-active/internal/events" "github.com/Tibz-Dankan/keep-active/internal/models" @@ -50,6 +52,15 @@ func signIn(w http.ResponseWriter, r *http.Request) { return } + if os.Getenv("GO_ENV") == "testing" || os.Getenv("GO_ENV") == "staging" { + permission := models.Permissions{} + if err := permission.Set(user.ID); err != nil { + log.Println("Error setting permissions:", err) + } + } else { + events.EB.Publish("permissions", user) + } + userMap := map[string]interface{}{ "id": user.ID, "name": user.Name, @@ -66,8 +77,6 @@ func signIn(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) - - events.EB.Publish("permissions", user) } func SignInRoute(router *mux.Router) { diff --git a/internal/routes/auth/signup.go b/internal/routes/auth/signup.go index a14b126..dc8edcb 100644 --- a/internal/routes/auth/signup.go +++ b/internal/routes/auth/signup.go @@ -2,7 +2,9 @@ package auth import ( "encoding/json" + "log" "net/http" + "os" "github.com/Tibz-Dankan/keep-active/internal/events" "github.com/Tibz-Dankan/keep-active/internal/models" @@ -35,8 +37,7 @@ func signUp(w http.ResponseWriter, r *http.Request) { return } - // TODO: To implement more scalable approach to set user roles - err = user.SetRole("client") + err = user.SetRole("user") if err != nil { services.AppError(err.Error(), 400, w) return @@ -55,6 +56,16 @@ func signUp(w http.ResponseWriter, r *http.Request) { return } + user.ID = userId + if os.Getenv("GO_ENV") == "testing" || os.Getenv("GO_ENV") == "staging" { + permission := models.Permissions{} + if err := permission.Set(user.ID); err != nil { + log.Println("Error setting permissions:", err) + } + } else { + events.EB.Publish("permissions", user) + } + newUser := map[string]interface{}{ "id": userId, "name": user.Name, @@ -71,9 +82,6 @@ func signUp(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(response) - - user.ID = userId - events.EB.Publish("permissions", user) } func SignUpRoute(router *mux.Router) { diff --git a/internal/routes/feedback/postFeedback.go b/internal/routes/feedback/postFeedback.go index 2af1a39..618b084 100644 --- a/internal/routes/feedback/postFeedback.go +++ b/internal/routes/feedback/postFeedback.go @@ -2,7 +2,9 @@ package feedback import ( "encoding/json" + "log" "net/http" + "os" "github.com/Tibz-Dankan/keep-active/internal/events" "github.com/Tibz-Dankan/keep-active/internal/middlewares" @@ -37,6 +39,16 @@ func postFeedback(w http.ResponseWriter, r *http.Request) { return } + user := models.User{ID: userId} + if os.Getenv("GO_ENV") == "testing" || os.Getenv("GO_ENV") == "staging" { + permission := models.Permissions{} + if err := permission.Set(user.ID); err != nil { + log.Println("Error setting permissions:", err) + } + } else { + events.EB.Publish("permissions", user) + } + response := map[string]interface{}{ "status": "success", "message": "Thank very much for your feedback", @@ -46,9 +58,6 @@ func postFeedback(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(response) - - user := models.User{ID: userId} - events.EB.Publish("permissions", user) } func PostFeedbackRoute(router *mux.Router) { diff --git a/internal/routes/request/postRequestTime.go b/internal/routes/request/postRequestTime.go index 6168891..73b2125 100644 --- a/internal/routes/request/postRequestTime.go +++ b/internal/routes/request/postRequestTime.go @@ -4,6 +4,7 @@ import ( "encoding/json" "log" "net/http" + "os" "time" "github.com/Tibz-Dankan/keep-active/internal/events" @@ -48,6 +49,17 @@ func postRequestTime(w http.ResponseWriter, r *http.Request) { return } + userId, _ := r.Context().Value(middlewares.UserIDKey).(string) + user := models.User{ID: userId} + if os.Getenv("GO_ENV") == "testing" || os.Getenv("GO_ENV") == "staging" { + permission := models.Permissions{} + if err := permission.Set(user.ID); err != nil { + log.Println("Error setting permissions:", err) + } + } else { + events.EB.Publish("permissions", user) + } + response := map[string]interface{}{ "status": "success", "message": "Request Time Created successfully", @@ -58,9 +70,6 @@ func postRequestTime(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(response) - userId, _ := r.Context().Value(middlewares.UserIDKey).(string) - user := models.User{ID: userId} - events.EB.Publish("permissions", user) } func PostRequestTimeRoute(router *mux.Router) { diff --git a/tests/auth/updateUserDetails_test.go b/tests/auth/updateUserDetails_test.go new file mode 100644 index 0000000..acb13dd --- /dev/null +++ b/tests/auth/updateUserDetails_test.go @@ -0,0 +1,150 @@ +package auth_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Tibz-Dankan/keep-active/internal/models" + "github.com/Tibz-Dankan/keep-active/tests/setup" +) + +func TestMissingUpdateUserDetailFields(t *testing.T) { + setup.ClearAllTables() + + var label string = "Expects 400 with missing name/email" + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + user := models.User{Name: "username", Email: "user@gmail.com", Password: "password"} + + userId, err := user.Create(user) + if err != nil { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + + signInPayload := []byte(`{"email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(signInPayload)) + response = setup.ExecuteRequest(req) + + json.Unmarshal(response.Body.Bytes(), &body) + + token, ok := body["accessToken"] + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + accessToken, _ := token.(string) + bearerToken := fmt.Sprintf("Bearer %s", accessToken) + + path := fmt.Sprintf("/api/v1/auth/user/update/%s", userId) + + payload = []byte(`{"name":"username","email":""}`) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", bearerToken) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) + + payload = []byte(`{"name":"","email":"user@gmail.com"}`) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", bearerToken) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestUpdatingToAlreadyExistingEmail(t *testing.T) { + setup.ClearAllTables() + + var label string = "Expects 400 trying to update to already existing email" + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + userOne := models.User{Name: "username1", Email: "user1@gmail.com", Password: "password"} + userTwo := models.User{Name: "username2", Email: "user2@gmail.com", Password: "password"} + + userIdOne, err := userOne.Create(userOne) + if err != nil { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + + if _, err := userTwo.Create(userTwo); err != nil { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + + signInPayload := []byte(`{"email":"user1@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(signInPayload)) + response = setup.ExecuteRequest(req) + + json.Unmarshal(response.Body.Bytes(), &body) + + token, ok := body["accessToken"] + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + accessToken, _ := token.(string) + bearerToken := fmt.Sprintf("Bearer %s", accessToken) + + path := fmt.Sprintf("/api/v1/auth/user/update/%s", userIdOne) + + payload = []byte(`{"name":"username","email":"user2@gmail.com"}`) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", bearerToken) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) + +} + +func TestSuccessfulUserDetailUpdate(t *testing.T) { + setup.ClearAllTables() + + var label string = "Expects 200 on successful user details update" + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + user := models.User{Name: "username", Email: "user@gmail.com", Password: "password"} + + userId, err := user.Create(user) + if err != nil { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + + signInPayload := []byte(`{"email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(signInPayload)) + response = setup.ExecuteRequest(req) + + json.Unmarshal(response.Body.Bytes(), &body) + + token, ok := body["accessToken"] + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + accessToken, _ := token.(string) + bearerToken := fmt.Sprintf("Bearer %s", accessToken) + + path := fmt.Sprintf("/api/v1/auth/user/update/%s", userId) + + payload = []byte(`{"name":"username2","email":"user2@gmail.com"}`) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", bearerToken) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusOK, response.Code) +} diff --git a/tests/main_test.go b/tests/main_test.go index 392a042..44a1861 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -4,16 +4,25 @@ import ( "os" "testing" + "github.com/Tibz-Dankan/keep-active/internal/events/publishers" + "github.com/Tibz-Dankan/keep-active/internal/events/subscribers" "github.com/Tibz-Dankan/keep-active/internal/models" + "github.com/Tibz-Dankan/keep-active/internal/schedulers" "github.com/Tibz-Dankan/keep-active/tests/setup" ) func TestMain(m *testing.M) { models.DBAutoMigrate() + go schedulers.InitSchedulers() + go subscribers.InitEventSubscribers() + publishers.InitEventPublishers() + code := m.Run() setup.ClearAllTables() os.Exit(code) + + select {} } From 74ad8bf77dba24ed0635b9f1354cc0f9dbe39154 Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Sun, 25 Aug 2024 23:49:35 +0300 Subject: [PATCH 09/10] test: add integration tests for change password endpoint --- tests/auth/changePassword_test.go | 183 ++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 tests/auth/changePassword_test.go diff --git a/tests/auth/changePassword_test.go b/tests/auth/changePassword_test.go new file mode 100644 index 0000000..2d259d1 --- /dev/null +++ b/tests/auth/changePassword_test.go @@ -0,0 +1,183 @@ +package auth_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Tibz-Dankan/keep-active/internal/models" + "github.com/Tibz-Dankan/keep-active/tests/setup" +) + +func TestMissingPasswordsFields(t *testing.T) { + setup.ClearAllTables() + + var label string = "Expects 400 with missing passwords" + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + user := models.User{Name: "username", Email: "user@gmail.com", Password: "password"} + + userId, err := user.Create(user) + if err != nil { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + + signInPayload := []byte(`{"email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(signInPayload)) + response = setup.ExecuteRequest(req) + + json.Unmarshal(response.Body.Bytes(), &body) + + token, ok := body["accessToken"] + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + accessToken, _ := token.(string) + bearerToken := fmt.Sprintf("Bearer %s", accessToken) + + path := fmt.Sprintf("/api/v1/auth/user/update-password/%s", userId) + + payload = []byte(`{"currentPassword":"password","newPassword":""}`) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", bearerToken) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) + + payload = []byte(`{"currentPassword":"","newPassword":"newPassword"}`) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", bearerToken) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestSimilarPasswords(t *testing.T) { + setup.ClearAllTables() + + var label string = "Expects 400 with current password similar to new password" + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + user := models.User{Name: "username", Email: "user@gmail.com", Password: "password"} + + userId, err := user.Create(user) + if err != nil { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + + signInPayload := []byte(`{"email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(signInPayload)) + response = setup.ExecuteRequest(req) + + json.Unmarshal(response.Body.Bytes(), &body) + + token, ok := body["accessToken"] + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + accessToken, _ := token.(string) + bearerToken := fmt.Sprintf("Bearer %s", accessToken) + + path := fmt.Sprintf("/api/v1/auth/user/update-password/%s", userId) + + payload = []byte(`{"currentPassword":"password1","newPassword":"password1"}`) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", bearerToken) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestNewPasswordSimilarToSavedOne(t *testing.T) { + setup.ClearAllTables() + + var label string = "Expects 400 with new password similar to saved password" + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + user := models.User{Name: "username", Email: "user@gmail.com", Password: "password"} + + userId, err := user.Create(user) + if err != nil { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + + signInPayload := []byte(`{"email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(signInPayload)) + response = setup.ExecuteRequest(req) + + json.Unmarshal(response.Body.Bytes(), &body) + + token, ok := body["accessToken"] + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + accessToken, _ := token.(string) + bearerToken := fmt.Sprintf("Bearer %s", accessToken) + + path := fmt.Sprintf("/api/v1/auth/user/update-password/%s", userId) + + payload = []byte(`{"currentPassword":"password","newPassword":"password"}`) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", bearerToken) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusBadRequest, response.Code) +} + +func TestSuccessfulChangePassword(t *testing.T) { + setup.ClearAllTables() + + var label string = "Expects 200 on successful change password" + var payload []byte + var req *http.Request + var response *httptest.ResponseRecorder + var body map[string]interface{} + + user := models.User{Name: "username", Email: "user@gmail.com", Password: "password"} + + userId, err := user.Create(user) + if err != nil { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", err) + return + } + + signInPayload := []byte(`{"email":"user@gmail.com","password":"password"}`) + req, _ = http.NewRequest("POST", "/api/v1/auth/signin", bytes.NewBuffer(signInPayload)) + response = setup.ExecuteRequest(req) + + json.Unmarshal(response.Body.Bytes(), &body) + + token, ok := body["accessToken"] + if !ok { + fmt.Printf("=== FAIL: %s\n", label) + t.Errorf("Expects accessToken. Got %v\n", token) + } + accessToken, _ := token.(string) + bearerToken := fmt.Sprintf("Bearer %s", accessToken) + + path := fmt.Sprintf("/api/v1/auth/user/update-password/%s", userId) + + payload = []byte(`{"currentPassword":"password","newPassword":"newPassword"}`) + req, _ = http.NewRequest("PATCH", path, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", bearerToken) + response = setup.ExecuteRequest(req) + setup.CheckResponseCode(t, label, http.StatusOK, response.Code) +} From 1e58b20e5006d64e86974654b32c921c44935998 Mon Sep 17 00:00:00 2001 From: Tibz-Dankan Date: Mon, 26 Aug 2024 15:45:32 +0300 Subject: [PATCH 10/10] CI/CD: add staging tests workflow spec --- .github/workflows/deploy-to-render.yaml | 8 ++++-- .github/workflows/tests.yaml | 37 +++++++++++++++++++++++++ Makefile | 8 ++++++ 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/deploy-to-render.yaml b/.github/workflows/deploy-to-render.yaml index 730f0ac..c339ab2 100644 --- a/.github/workflows/deploy-to-render.yaml +++ b/.github/workflows/deploy-to-render.yaml @@ -1,9 +1,11 @@ name: Deploy To Render on: - push: - branches: - - main + workflow_dispatch: + workflow_run: + workflows: ["Tests"] + types: + - completed jobs: deploy: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..c838495 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,37 @@ +name: Tests + +on: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + env: + APPCRONS_STAG_DSN: ${{ secrets.APPCRONS_STAG_DSN }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.23" + + - name: Install dependencies + run: make install + + - name: Run tests + run: make stage # Runs tests in the staging environment + + - name: Trigger deployment + if: success() + run: | + curl -X POST \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/deploy-to-render.com.yml/dispatches \ + -d '{"ref": "${{ github.ref_name }}"}' diff --git a/Makefile b/Makefile index 044c21a..b71b550 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,25 @@ CMD_DIR := ./cmd TEST_DIR := ./tests +# Runs the development server .PHONY: run run: @echo "Starting development server..." @GO_ENV=development go run $(CMD_DIR) +# Runs tests in the development environment .PHONY: test test: @echo "Running tests with GO_ENV=testing..." @GO_ENV=testing go test -v $(TEST_DIR)/... +# Runs tests in the staging environment +.PHONY: stage +stage: + @echo "Running tests with GO_ENV=staging..." + @GO_ENV=staging go test -v $(TEST_DIR)/... +# Installs the packages .PHONY: install install: @echo "Installing dependencies..."