diff --git a/.env.template b/.env.template index 6ffa65b..409a1e5 100644 --- a/.env.template +++ b/.env.template @@ -1,6 +1,5 @@ PORT= -SUPABASE_DB_PASSWORD= SUPABASE_URI= # true | false diff --git a/.github/workflows/client-test.yaml b/.github/workflows/client-test.yaml index 750deb7..a7da4b1 100644 --- a/.github/workflows/client-test.yaml +++ b/.github/workflows/client-test.yaml @@ -1,9 +1,9 @@ -name: Client Test Workflow +name: Client Test on: pull_request: branches: - - main + - develop jobs: test: diff --git a/.github/workflows/go-build.yaml b/.github/workflows/go-build.yaml new file mode 100644 index 0000000..cdc4d3d --- /dev/null +++ b/.github/workflows/go-build.yaml @@ -0,0 +1,50 @@ +name: Backend - Build + +on: + pull_request: + branches: + - develop + workflow_dispatch: + +jobs: + build: + name: Build Application + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ">=1.23" + + - name: Cache Go Modules + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-build- + + - name: Check Go Version + run: go version + + - name: Run Build + env: + PORT: ${{ vars.PORT }} + SUPABASE_URI: ${{ secrets.SUPABASE_URI }} + RUN_MIGRATIONS: ${{ vars.RUN_MIGRATIONS }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + run: | + if ! go build -tags netgo -ldflags '-s -w' -o app; then + echo "Build failed. Please check the logs." + exit 1 + fi + + - name: Upload Build Artifact + uses: actions/upload-artifact@v4 + with: + name: backend-app + path: app diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml index 101e530..5357817 100644 --- a/.github/workflows/go-test.yaml +++ b/.github/workflows/go-test.yaml @@ -1,9 +1,9 @@ -name: Go Test Workflow +name: Backend Test on: pull_request: branches: - - main + - develop jobs: test: @@ -20,7 +20,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.23 + go-version: ">=1.23" - name: Check Go Version run: go version @@ -29,9 +29,8 @@ jobs: env: SUPABASE_URI: ${{ secrets.SUPABASE_URI }} SECRET_KEY: ${{ secrets.SECRET_KEY }} - ENV: ${{ secrets.ENV }} - CLIENT_URL: ${{ secrets.CLIENT_URL }} + ENV: ${{ vars.ENV }} EMAIL: ${{ secrets.EMAIL }} EMAIL_APP_PASSWORD: ${{ secrets.EMAIL_APP_PASSWORD }} - TEST_EMAIL: ${{ secrets.TEST_EMAIL }} + TEST_EMAIL: ${{ vars.TEST_EMAIL }} run: go test ./... -v diff --git a/configs/email_config.go b/configs/email_config.go index 76bb680..d2edd4b 100644 --- a/configs/email_config.go +++ b/configs/email_config.go @@ -1,7 +1,7 @@ package configs import ( - "log" + "errors" "net/smtp" "os" ) @@ -13,12 +13,15 @@ var ( const SMTPADDRESS = "smtp.gmail.com:587" -func ConfigureEmail() { +func ConfigureEmail() error { EMAIL := os.Getenv("EMAIL") PASSWORD := os.Getenv("EMAIL_APP_PASSWORD") + if EMAIL == "" || PASSWORD == "" { - log.Fatal("EMAIL or EMAIL_APP_PASSWORD must be set") + return errors.New("EMAIL or EMAIL_APP_PASSWORD must be set") } SMTPAuth = smtp.PlainAuth("", EMAIL, PASSWORD, "smtp.gmail.com") + Email = EMAIL + return nil } diff --git a/configs/email_config_test.go b/configs/email_config_test.go deleted file mode 100644 index 3b32bdf..0000000 --- a/configs/email_config_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package configs - -import ( - "bytes" - "log" - "testing" -) - -func TestConfigureEmail(t *testing.T) { - t.Run("ConfigureEmail_Success", func(t *testing.T) { - var buf bytes.Buffer - log.SetOutput(&buf) - - ConfigureEmail() - - if buf.String() != "" { - t.Errorf("Expected no log output, but got: %s", buf.String()) - } - - if SMTPAuth == nil { - t.Error("Expected SMTPAuth to be set, but it is nil") - } - }) - -} diff --git a/configs/load_env_test.go b/configs/load_env_test.go deleted file mode 100644 index c3bd75f..0000000 --- a/configs/load_env_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package configs - -import ( - "fmt" - "os" - "testing" -) - -func TestLoadEnv(t *testing.T) { - envFile := ".env" - name := "TEST_VAR" - - t.Run("LoadEnv_Matches", func(t *testing.T) { - os.Unsetenv(name) - - err := os.WriteFile(envFile, []byte(fmt.Sprintf("%s=hello\n", name)), 0644) - if err != nil { - t.Fatalf("Failed to create mock .env file: %v", err) - } - - err = LoadEnv() - - if err != nil { - t.Errorf("Expected no error, but got: %v", err) - } - - if value := os.Getenv(name); value == "hello" { - t.Logf("Expected %s to be 'hello' and got '%s'", name, value) - } - }) - - t.Run("LoadEnv_No_Match", func(t *testing.T) { - os.Unsetenv(name) - - err := os.WriteFile(envFile, []byte(fmt.Sprintf("%s=123\n", name)), 0644) - if err != nil { - t.Fatalf("Failed to create mock .env file: %v", err) - } - - err = LoadEnv() - - if err != nil { - t.Errorf("Expected no error, but got: %v", err) - } - - if value := os.Getenv(name); value != "123" { - t.Errorf("Expected %s to be '123', but got '%s'", name, value) - } - }) - - t.Run("LoadEnv_File_NotFound", func(t *testing.T) { - os.Remove(".env") - - err := LoadEnv() - - if err == nil { - t.Errorf("Expected an error when .env file is missing, but got nil: %v", err) - } - }) -} diff --git a/configs/setup_cors.go b/configs/setup_cors.go index 4f28f69..451442a 100644 --- a/configs/setup_cors.go +++ b/configs/setup_cors.go @@ -1,21 +1,23 @@ package configs import ( - "log" + "errors" "os" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/cors" ) -func SetupCORS(app *fiber.App) { +func SetupCORS(app *fiber.App) error { CLIENT_URL := os.Getenv("CLIENT_URL") if CLIENT_URL == "" { - log.Fatal("CLIENT_URL must be set") + return errors.New("CLIENT_URL must be set") } app.Use(cors.New(cors.Config{ AllowOrigins: []string{CLIENT_URL}, AllowCredentials: true, })) + + return nil } diff --git a/configs/setup_cors_test.go b/configs/setup_cors_test.go deleted file mode 100644 index 7999293..0000000 --- a/configs/setup_cors_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package configs - -import ( - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/gofiber/fiber/v3" -) - -func TestSetupCORS(t *testing.T) { - envName := "CLIENT_URL" - testOrigin := os.Getenv(envName) - - app := fiber.New() - SetupCORS(app) - - app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello world!") - }) - - t.Run("SetupCORS_Valid_Origin", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("Origin", testOrigin) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Error making test request: %v", err) - } - - corsOrigin := resp.Header.Get("Access-Control-Allow-Origin") - if corsOrigin != testOrigin { - t.Errorf("Expected Access-Control-Allow-Origin to be '%s', got '%s'", testOrigin, corsOrigin) - } - }) - - t.Run("SetupCORS_Invalid_Origin", func(t *testing.T) { - otherOrigin := "http://test.com" - - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("Origin", otherOrigin) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Error making test request: %v", err) - } - - corsOrigin := resp.Header.Get("Access-Control-Allow-Origin") - if corsOrigin == testOrigin { - t.Errorf("Expected Access-Control-Allow-Origin to be not '%s', got '%s'", testOrigin, corsOrigin) - } - }) -} diff --git a/database/connection_test.go b/database/connection_test.go deleted file mode 100644 index fa41400..0000000 --- a/database/connection_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package database - -import ( - "bytes" - "log" - "testing" -) - -func TestConnectDB(t *testing.T) { - var buf bytes.Buffer - log.SetOutput(&buf) - - ConnectDB() - - if buf.String() != "" { - t.Errorf("Expected no log output, but got: %s", buf.String()) - } - - if Instance == nil { - t.Error("Expected Instance to be set, but it is nil") - } else { - t.Logf("Instance: %v", Instance.Name()) - } -} diff --git a/utils/email_service_test.go b/utils/email_service_test.go deleted file mode 100644 index 3aeba0f..0000000 --- a/utils/email_service_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package utils - -import ( - "os" - "testing" - - "github.com/Fingertips18/go-auth/configs" -) - -func TestEmailService(t *testing.T) { - toEmail := os.Getenv("TEST_EMAIL") - const username = "Test" - - configs.ConfigureEmail() - - t.Run("SendWelcomeEmail_Test", func(t *testing.T) { - if err := SendWelcomeEmail(toEmail, username); err != nil { - t.Errorf("Ërror sending welcome email: %v", err) - } else { - t.Log("welcome email sent") - } - }) - - t.Run("SendEmailVerification_Test", func(t *testing.T) { - if err := SendEmailVerification(toEmail, username, "1234"); err != nil { - t.Errorf("Ërror sending email verification: %v", err) - } else { - t.Log("Verification email sent") - } - }) - - t.Run("SendEmailRequestResetPassword_Test", func(t *testing.T) { - if err := SendEmailRequestResetPassword(toEmail, "reset-token"); err != nil { - t.Errorf("Ërror sending reset password request : %v", err) - } else { - t.Log("Successful reset password request email sent") - } - }) - - t.Run("SendEmailResetPasswordSuccess_Test", func(t *testing.T) { - if err := SendEmailResetPasswordSuccess(toEmail, username); err != nil { - t.Errorf("Ërror sending reset password success : %v", err) - } else { - t.Log("Reset password successful email sent") - } - }) - - t.Run("SendWelcomeEmail_Test", func(t *testing.T) { - if err := SendWelcomeEmail(toEmail, username); err != nil { - t.Errorf("Ërror sending welcome email: %v", err) - } else { - t.Log("welcome email sent") - } - - }) -} diff --git a/utils/password_hash_test.go b/utils/password_hash_test.go deleted file mode 100644 index d43fbeb..0000000 --- a/utils/password_hash_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package utils - -import "testing" - -func TestHashPassword(t *testing.T) { - password := "secret" - - hashPass, err := HashPassword(password) - if err != nil { - t.Fatalf("Error hashing password: %v", err) - } - - if len(hashPass) == 0 || !_isValidBcryptHash(hashPass) { - t.Errorf("Hashed password is not a valid bcrypt hash: %s", hashPass) - } - - hashPass2, err := HashPassword(password) - if err != nil { - t.Fatalf("Error hashing 2nd password: %v", err) - } - - if hashPass == hashPass2 { - t.Errorf("Hashing the same password produces the same hash: %s", hashPass) - } -} - -func TestVerifyPassword(t *testing.T) { - password := "secret" - - hashPass, err := HashPassword(password) - if err != nil { - t.Fatalf("Error hashing password: %v", err) - } - - if err := VerifyPassword([]byte(hashPass), []byte(password)); err != nil { - t.Errorf("Hash does not match password: %v", err) - } -} - -func _isValidBcryptHash(hash string) bool { - if len(hash) < 60 { - return false - } - return hash[:4] == "$2a$" || hash[:4] == "$2b$" || hash[:4] == "$2y$" -} diff --git a/utils/session_token_test.go b/utils/session_token_test.go deleted file mode 100644 index 8057e16..0000000 --- a/utils/session_token_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package utils - -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "strings" - "testing" - "time" - - "github.com/gofiber/fiber/v3" -) - -type JWTHeader struct { - Alg string `json:"alg"` - Typ string `json:"typ"` -} - -type JWTPayload struct { - Sub string `json:"sub"` - Name string `json:"name"` - Admin bool `json:"admin"` - Exp int64 `json:"exp"` -} - -func TestToken(t *testing.T) { - // Credentials for jwt headers - id := "@asd123" - username := "test" - - envSecretName := "SECRET_KEY" - secret := os.Getenv(envSecretName) - - t.Run("GenerateJWTToken_Validate", func(t *testing.T) { - token, err := GenerateJWTToken(id, username) - if err != nil { - t.Errorf("Error generating token: %v", err) - } - - parts := strings.Split(token, ".") - if len(parts) != 3 { - t.Errorf("Invalid token: %v", token) - } - - headerPart := parts[0] - headerBytes, err := base64.RawURLEncoding.DecodeString(headerPart) - if err != nil { - t.Errorf("Error decoding JWT header: %v", err) - } - - var header JWTHeader - if err := json.Unmarshal(headerBytes, &header); err != nil { - t.Errorf("Error unmarshalling JWT header: %v", err) - } else { - t.Logf("JWT is signed header: %s", header) - } - - payloadPart := parts[1] - payloadBytes, err := base64.RawURLEncoding.DecodeString(payloadPart) - if err != nil { - t.Errorf("Error decoding JWT payload: %v", err) - } - - var payload JWTPayload - if err := json.Unmarshal(payloadBytes, &payload); err != nil { - t.Errorf("Error unmarshalling JWT header: %v", err) - } else { - t.Logf("JWT payloads: %v", payload) - } - - exp := time.Unix(payload.Exp, 0) - if time.Now().After(exp) { - t.Errorf("Error: Token has expired. Expiration time: %v", exp) - } else { - t.Logf("Token is still valid. Expiration time: %v", exp) - } - - signaturePart := parts[2] - signatureBytes, err := base64.RawURLEncoding.DecodeString(signaturePart) - if err != nil { - t.Errorf("Error decoding JWT signature: %v", err) - } - - dataToVerify := parts[0] + "." + parts[1] - h := hmac.New(sha256.New, []byte(secret)) - h.Write([]byte(dataToVerify)) - expectedSignature := h.Sum(nil) - - if hmac.Equal(signatureBytes, expectedSignature) { - t.Logf("JWT signature is valid: %s", expectedSignature) - } else { - t.Log("JWT signature is invalid") - } - }) - - t.Run("SetCookieToken_Validate", func(t *testing.T) { - app, token := _HandleCookieToken(t, id, username) - - // Simulate request to check if cookie token was set - req := httptest.NewRequest(http.MethodGet, "/set-cookie", nil) - req.AddCookie(&http.Cookie{Name: "token", Value: token}) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Error making set cookie request: %v", err) - } - - found := false - for _, cookie := range resp.Cookies() { - if cookie.Name == "token" { - found = true - t.Logf("Cookie 'token' is set with value: %s", cookie.Value) - - if cookie.Value != token { - t.Errorf("Expected cookie value to be %s but got %s", token, cookie.Value) - } - - if time.Now().After(cookie.Expires) { - t.Errorf("Error: Cookie has expired. Expiration time: %v", cookie.Expires) - } else { - t.Logf("Cookie is still valid. Expiration time: %v", cookie.Expires) - } - } - - } - - if !found { - t.Error("Cookie 'token' was not set in the response") - } - }) - - t.Run("ClearCookieToken_Validate", func(t *testing.T) { - app, token := _HandleCookieToken(t, id, username) - - // Simulate request to check if cookie token was cleared - req := httptest.NewRequest(http.MethodGet, "/clear-cookie", nil) - req.AddCookie(&http.Cookie{Name: "token", Value: token}) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Error making clear cookie request: %v", err) - } - - empty := false - for _, cookie := range resp.Cookies() { - if cookie.Name == "token" { - if cookie.Value == "" { - empty = true - } - - if cookie.Value == token { - t.Errorf("Expected cookie to be empty but got %v", cookie.Value) - } - - if time.Now().Before(cookie.Expires) { - t.Errorf("Error: Cookie should expired but got: %v", cookie.Expires) - } - } - - } - - if empty { - t.Log("No cookie was found") - } - }) - - t.Run("GenerateResetToken_Validate", func(t *testing.T) { - token, err := GenerateResetToken() - if err != nil { - t.Errorf("Error generating reset token: %v", err) - } - - if token == nil { - t.Fatalf("Expected a token, but got nil: %v", token) - } - - _, err = base64.URLEncoding.DecodeString(*token) - if err != nil { - t.Errorf("Error decoding base64 string: %v", err) - } - - expectedLength := 24 - if len(*token) != expectedLength { - t.Errorf("Expected reset token length to be %d, but got %d", expectedLength, len(*token)) - } - }) -} - -func _HandleCookieToken(t *testing.T, id string, username string) (*fiber.App, string) { - t.Helper() - - token, err := GenerateJWTToken(id, username) - if err != nil { - t.Errorf("Error generating token: %v", err) - } - - // Define dummy route to set cookie token - app := fiber.New() - app.Get("/set-cookie", func(c fiber.Ctx) error { - SetCookieToken(c, token) - return c.SendString("Cookie set!") - }) - - // Define dummy route to clear cookie token - app.Get("/clear-cookie", func(c fiber.Ctx) error { - ClearCookieToken(c) - return c.SendString("Cookies cleared!") - }) - - return app, token -}