Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ go.work
# Miscellaneous
todo.txt

# Logger
# Logs
logs/*.log
internal/handlers/logs/*.log

# Coverage
internal/handlers/coverage
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@ module github.com/milwad-dev/do-it
go 1.22.0

require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-chi/chi/v5 v5.2.0
github.com/go-playground/validator/v10 v10.25.0
github.com/go-sql-driver/mysql v1.8.1
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.8.0
github.com/stretchr/testify v1.9.0
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.4
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.32.0
)

require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
Expand All @@ -28,10 +33,9 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/redis/go-redis/v9 v9.8.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/swaggo/files v1.0.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down Expand Up @@ -36,6 +42,7 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
Expand All @@ -59,6 +66,8 @@ github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
Expand Down
27 changes: 27 additions & 0 deletions internal/handlers/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package handlers

import (
"github.com/DATA-DOG/go-sqlmock"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
"testing"
)

func TestNewDBHandler(t *testing.T) {
// Create a mock sql.DB
db, _, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()

// Create a mock redis client (no need to mock connection for this simple test)
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})

// Call constructor
handler := NewDBHandler(db, redisClient)

// Assertions
require.NotNil(t, handler)
require.Equal(t, db, handler.DB)
}
10 changes: 5 additions & 5 deletions internal/handlers/label_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ func (db *DBHandler) GetLatestLabels(w http.ResponseWriter, r *http.Request) {

query := `
SELECT l.id, l.title, l.color, l.created_at, l.updated_at, l.user_id,
u.id, u.name, COALESCE(u.email, ''), COALESCE(u.phone, ''), u.created_at
FROM labels l
JOIN users u ON l.user_id = u.id
ORDER BY l.created_at DESC
WHERE user_id = ?`
u.id, u.name, COALESCE(u.email, ''), COALESCE(u.phone, ''), u.created_at
FROM labels AS l
JOIN users AS u ON l.user_id = u.id
WHERE l.user_id = ?
ORDER BY l.created_at DESC`
rows, err := db.Query(query, userId)
if err != nil {
data["message"] = err.Error()
Expand Down
192 changes: 155 additions & 37 deletions internal/handlers/label_handler_test.go
Original file line number Diff line number Diff line change
@@ -1,58 +1,176 @@
package handlers

import (
"bytes"
"context"
"github.com/dgrijalva/jwt-go"
"github.com/go-chi/chi/v5"
"github.com/milwad-dev/do-it/internal/logger"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/require"
)

// Mock dbHandler struct to satisfy the dbHandler interface
type dbHandler struct{}
func TestGetLatestLabels_OK(t *testing.T) {
// Mock DB
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()

// Initialize logger in non-production mode for testing (prints JSON to stdout + file)
logger.InitLogger(false)

// Expectations
mock.ExpectQuery(regexp.QuoteMeta(`
SELECT l.id, l.title, l.color, l.created_at, l.updated_at, l.user_id,
u.id, u.name, COALESCE(u.email, ''), COALESCE(u.phone, ''), u.created_at
FROM labels AS l
JOIN users AS u ON l.user_id = u.id
WHERE l.user_id = ?
ORDER BY l.created_at DESC
`)).
WithArgs(float64(1)).
WillReturnRows(sqlmock.NewRows([]string{
"id", "title", "color", "created_at", "updated_at", "user_id",
"user_id", "name", "email", "phone", "user_created_at",
}).AddRow(
1, "Label Title", "#FF0000", "2025-01-01 10:00:00", "2025-01-02 10:00:00", 42,
42, "User Name", "user@example.com", "1234567890", "2024-12-31 09:00:00",
))

// Handler
h := &DBHandler{DB: db}

// Request setup
req := httptest.NewRequest(http.MethodGet, "/labels", nil)

// Add Chi route context with id param
req = callContext(req)

// Recorder & handler call
rr := httptest.NewRecorder()
h.GetLatestLabels(rr, req)

// Assert
require.Equal(t, http.StatusOK, rr.Code)
require.JSONEq(t, `{
"data": [
{
"id": 1,
"title": "Label Title",
"color": "#FF0000",
"created_at": "2025-01-01 10:00:00",
"updated_at": "2025-01-02 10:00:00",
"user": {
"id": 42,
"name": "User Name",
"email": "user@example.com",
"phone": "1234567890",
"emailVerified_at": "0001-01-01T00:00:00Z",
"phoneVerified_at": "0001-01-01T00:00:00Z",
"created_at": "2024-12-31 09:00:00",
"updated_at": ""
}
}
],
"status": "Success"
}`, rr.Body.String())
require.NoError(t, mock.ExpectationsWereMet())
}

func TestStoreLabel_OK(t *testing.T) {
// Setup mock DB
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()

// Initialize logger if needed
logger.InitLogger(false)

func TestStoreLabel(t *testing.T) {
// Create a mock dbHandler with any necessary dependencies
mockDB := &dbHandler{} // You may need to create a mock dbHandler
// Prepare expected query and arguments
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO labels (title, color, user_id) VALUES (?, ?, ?)")).
WithArgs("Test Label", "#FF00FF", float64(1)).
WillReturnResult(sqlmock.NewResult(1, 1))

// Create a sample label JSON body
labelJSON := []byte(`{"title": "Test Label", "color": "#FF0000"}`)
// Setup DBHandler with mock DB
h := &DBHandler{DB: db}

// Create a mock HTTP request with the sample label data
req, err := http.NewRequest("POST", "/labels", bytes.NewBuffer(labelJSON))
if err != nil {
t.Fatal(err)
}
// Create JSON request body
body := `{"title":"Test Label","color":"#FF00FF"}`

// Create a ResponseRecorder to record the response
req := httptest.NewRequest(http.MethodPost, "/labels", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")

// Add userID context (matching your GetUserIdFromContext)
req = callContext(req)

// Record response
rr := httptest.NewRecorder()

// Call the storeLabel handler function with the mock dbHandler and mock HTTP request
handler := http.HandlerFunc(mockDB.storeLabel)
handler.ServeHTTP(rr, req)
// Call handler
h.StoreLabel(rr, req)

// Check response code
require.Equal(t, http.StatusOK, rr.Code)

// Check the status code returned by the handler
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
// Check response body JSON
expected := `{"data":{"message":"The label store successfully."}, "status":"Success"}`
require.JSONEq(t, expected, rr.Body.String())

// Check if the response body contains the expected message
expectedResponse := `"message":"The label stored successfully."`
if rr.Body.String() != expectedResponse {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expectedResponse)
}
// Ensure all expectations were met
require.NoError(t, mock.ExpectationsWereMet())
}

// Mock implementation of storeLabel function to satisfy the dbHandler interface
func (db *dbHandler) storeLabel(w http.ResponseWriter, r *http.Request) {
data := make(map[string]string)
data["message"] = "The label stored successfully."
jsonResponse(w, data, http.StatusOK)
func TestDeleteLabel_OK(t *testing.T) {
// Mock DB
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()

// Expectations
mock.ExpectQuery(`SELECT count\(\*\) FROM labels WHERE id = \? AND user_id = \?`).
WithArgs("42", float64(1)).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))

mock.ExpectExec(`DELETE FROM labels WHERE id = \? AND user_id = \?`).
WithArgs("42", float64(1)).
WillReturnResult(sqlmock.NewResult(0, 1))

// Handler
h := &DBHandler{DB: db}

// Request setup
req := httptest.NewRequest(http.MethodDelete, "/labels/42", nil)

// Add Chi route context with id param
req = callContext(req)

// Recorder & handler call
rr := httptest.NewRecorder()
h.DeleteLabel(rr, req)

// Assert
require.Equal(t, http.StatusOK, rr.Code)
require.JSONEq(t, `{"data": {
"message":"The label deleted successfully."
},
"status": "Success"
}`, rr.Body.String())
require.NoError(t, mock.ExpectationsWereMet())
}

// Mock implementation of jsonResponse function for testing purposes
func jsonResponse(w http.ResponseWriter, data map[string]string, statusCode int) {
w.WriteHeader(statusCode)
for key, value := range data {
w.Write([]byte(`"` + key + `":"` + value + `"`))
}
func callContext(req *http.Request) *http.Request {
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "42")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

// Add JWT claims to context
req = req.WithContext(context.WithValue(req.Context(), "userID", jwt.MapClaims{
"user_id": float64(1),
}))
return req
}
Loading