From 976a017b4f2663f2a67a5c2a6ec6e0dcf6778fd8 Mon Sep 17 00:00:00 2001 From: phoenix Date: Fri, 12 Sep 2025 18:30:15 +0000 Subject: [PATCH] added test (#5) Reviewed-on: https://git.kundeng.us/phoenix/textsender-auth/pulls/5 Co-authored-by: phoenix Co-committed-by: phoenix --- .gitea/workflows/workflow.yaml | 51 +++++++++++-- cmd/api/main_test.go | 82 +++++++++++++++++++++ go.mod | 7 ++ go.sum | 15 +++- internal/config/config.go | 4 +- internal/db/connection.go | 9 ++- internal/handler/mock_store.go | 114 ++++++++++++++++++++++++++++++ internal/handler/register_test.go | 45 ++++++++++++ internal/model/user.go | 2 +- 9 files changed, 319 insertions(+), 10 deletions(-) create mode 100644 cmd/api/main_test.go create mode 100644 internal/handler/mock_store.go create mode 100644 internal/handler/register_test.go diff --git a/.gitea/workflows/workflow.yaml b/.gitea/workflows/workflow.yaml index 6c82b3c..8ef3f18 100644 --- a/.gitea/workflows/workflow.yaml +++ b/.gitea/workflows/workflow.yaml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-24.04 # You can change this to macos-latest or windows-latest if needed steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: '1.25.1' # You can specify a specific version or 'stable' @@ -25,9 +25,52 @@ jobs: -X main.commit=${{ github.sha }} \ -X main.date=$(date +%Y-%m-%dT%H:%M:%S%z)" \ -o textsender-auth cmd/api/main.go + test: + name: Test + runs-on: ubuntu-24.04 + services: + postgres: + image: postgres:17.5 + env: + POSTGRES_USER: ${{ secrets.DB_TEST_USER }} + POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }} + POSTGRES_DB: ${{ secrets.DB_TEST_NAME }} + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Test - run: go test -v ./... + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.25.1' + + - name: Run tests + env: + DB_NAME: ${{ secrets.DB_TEST_NAME }} + DB_USER: ${{ secrets.DB_TEST_USER }} + DB_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }} + DB_HOST: postgres + DB_PORT: 5432 + DB_SSLMODE: disable + run: | + echo "Parent directory" + echo `pwd` + + echo "DB_NAME=$DB_NAME" > .env + echo "DB_USER=$DB_USER" >> .env + echo "DB_PASSWORD=$DB_PASSWORD" >> .env + echo "DB_HOST=$DB_HOST" >> .env + echo "DB_PORT=$DB_PORT" >> .env + echo "DB_SSLMODE=$DB_SSLMODE" >> .env + + go test -v ./... # - name: Run gofmt (optional) # Uncomment to check code formatting with gofmt diff --git a/cmd/api/main_test.go b/cmd/api/main_test.go new file mode 100644 index 0000000..19363df --- /dev/null +++ b/cmd/api/main_test.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "path" + "testing" + + "github.com/gorilla/mux" + "github.com/joho/godotenv" + + "git.kundeng.us/phoenix/textsender-auth/internal/config" + "git.kundeng.us/phoenix/textsender-auth/internal/db" + "git.kundeng.us/phoenix/textsender-auth/internal/handler" + "git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint" + "git.kundeng.us/phoenix/textsender-auth/internal/model" +) + +var testRouter *mux.Router + +func TestMain(m *testing.M) { + cfg := load() + + db, err := db.NewDatabase(cfg.GetDBConnString()) + if err != nil { + fmt.Println(err.Error()) + panic("Failed to initialize database") + } + defer db.Close() + + ctx := context.Background() + err = db.ResetDatabase(ctx) + if err != nil { + fmt.Println(err.Error()) + panic("Failed to initialize database") + } + + userStore := model.NewUserStore(db.Pool) + userHandler := handler.NewUserHandler(userStore) + + testRouter = mux.NewRouter() + testRouter.HandleFunc(endpoint.Register, userHandler.Register).Methods("POST") + + code := m.Run() + os.Exit(code) +} + +func load() *config.Config { + resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit") + port := flag.String("port", config.Port, "Server port") + flag.Parse() + + cwd, _ := os.Getwd() + envPath := path.Join(cwd, ".env") + + err := godotenv.Load(envPath) + if err != nil { + envPath = path.Join(cwd, "../..", ".env") + if err := godotenv.Load(envPath); err != nil { + panic("Error loading .env file: " + err.Error()) + } + } + + unpackedConnString := config.UnpackDBConnString() + dbConnString := unpackedConnString.Parse() + + return &config.Config{ + DBConnString: dbConnString, + ServerPort: *port, + ResetDB: *resetDb, + } +} + +func resetTestDB(t *testing.T) { + t.Helper() + _, err := db.Pool.Exec(context.Background(), "DELETE FROM users") + if err != nil { + t.Fatalf("Failed to reset test database: %v", err) + } +} diff --git a/go.mod b/go.mod index c0b4ba7..4ceaf7a 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,22 @@ go 1.25.1 require ( github.com/go-chi/chi/v5 v5.2.3 github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 github.com/jackc/pgx/v5 v5.7.5 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.42.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/text v0.29.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 21e2d71..8669674 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -5,6 +6,8 @@ github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -15,13 +18,19 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= @@ -29,6 +38,8 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index 88ad34c..b512ff3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -41,7 +41,7 @@ func Load() *Config { log.Fatal("Error loading .env file") } - unpackedConnString := unpackDBConnString() + unpackedConnString := UnpackDBConnString() dbConnString := unpackedConnString.Parse() return &Config{ @@ -51,7 +51,7 @@ func Load() *Config { } } -func unpackDBConnString() (connInfo ConnectionInfo) { +func UnpackDBConnString() (connInfo ConnectionInfo) { username := os.Getenv("DB_USER") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") diff --git a/internal/db/connection.go b/internal/db/connection.go index 91036dc..877b19e 100644 --- a/internal/db/connection.go +++ b/internal/db/connection.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "path" "strings" "time" @@ -82,7 +83,13 @@ func (db *Database) ResetDatabase(ctx context.Context) error { schemaContent, err := os.ReadFile("migrations/schema.sql") if err != nil { - return fmt.Errorf("error reading schema file: %v", err) + log.Println("Default migrations not found. Checking different directory") + cwd, _ := os.Getwd() + migrationsPath := path.Join(cwd, "../..", "migrations/schema.sql") + schemaContent, err = os.ReadFile(migrationsPath) + if err != nil { + return fmt.Errorf("error reading schema file: %v", err) + } } statements := strings.Split(string(schemaContent), ";") diff --git a/internal/handler/mock_store.go b/internal/handler/mock_store.go new file mode 100644 index 0000000..88bbaa2 --- /dev/null +++ b/internal/handler/mock_store.go @@ -0,0 +1,114 @@ +package handler + +import ( + "context" + "errors" + "sync" + + "github.com/google/uuid" + + "git.kundeng.us/phoenix/textsender-auth/internal/model" +) + +type MockUserStore struct { + Users map[uuid.UUID]*model.User + UsersByUsername map[string]*model.User + mu sync.RWMutex + Error error // Optional: simulate errors +} + +func NewMockUserStore() *MockUserStore { + return &MockUserStore{ + Users: make(map[uuid.UUID]*model.User), + UsersByUsername: make(map[string]*model.User), + } +} + +func (m *MockUserStore) CreateUser(ctx context.Context, user *model.User) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return m.Error + } + + if user.Id == uuid.Nil { + user.Id = uuid.New() + } + + if _, exists := m.UsersByUsername[user.Username]; exists { + return errors.New("User with email already exists") + } + + m.Users[user.Id] = user + m.UsersByUsername[user.Username] = user + return nil +} + +func (m *MockUserStore) GetUserByID(ctx context.Context, id uuid.UUID) (*model.User, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return nil, m.Error + } + + if m.Error != nil { + return nil, m.Error + } + + user, exists := m.Users[id] + if !exists { + return nil, errors.New("User not found") + } + + return user, nil +} + +func (m *MockUserStore) GetUserByUsername(ctx context.Context, username string) (*model.User, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return nil, m.Error + } + + user, exists := m.UsersByUsername[username] + if !exists { + return nil, errors.New("User not found") + } + + return user, nil +} + +func (m *MockUserStore) GetAllUsers(ctx context.Context) ([]*model.User, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return nil, m.Error + } + + users := make([]*model.User, 0, len(m.Users)) + for _, user := range m.Users { + users = append(users, user) + } + + return users, nil +} + +func (m *MockUserStore) UserExists(ctx context.Context, username string) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return false, m.Error + } + + _, exists := m.UsersByUsername[username] + if !exists { + return exists, errors.New("User not found") + } + + return exists, nil +} diff --git a/internal/handler/register_test.go b/internal/handler/register_test.go new file mode 100644 index 0000000..6906630 --- /dev/null +++ b/internal/handler/register_test.go @@ -0,0 +1,45 @@ +package handler + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "git.kundeng.us/phoenix/textsender-auth/internal/db" + "git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint" + "git.kundeng.us/phoenix/textsender-auth/internal/model" + + "github.com/stretchr/testify/assert" +) + +func TestCreateUserWithMock(t *testing.T) { + mockstore := NewMockUserStore() + handler := NewUserHandler(mockstore) + + testUser := model.User{Username: "ghost", PhoneNumber: "+1234567890", Password: "dfgdffddfd"} + jsonValue, _ := json.Marshal(testUser) + + req, _ := http.NewRequest("POST", endpoint.Register, strings.NewReader(string(jsonValue))) + rr := httptest.NewRecorder() + + handler.Register(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response RegisterResponse + err := json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err) + + assert.NotNil(t, response.Data[0].Id, "Id should not be nil") +} + +func resetTestDB(t *testing.T) { + t.Helper() + _, err := db.Pool.Exec(context.Background(), "DELETE FROM users") + if err != nil { + t.Fatalf("Failed to reset test database: %v", err) + } +} diff --git a/internal/model/user.go b/internal/model/user.go index 62267c1..1efefa4 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -21,7 +21,7 @@ type UserStore interface { GetUserByID(ctx context.Context, id uuid.UUID) (*User, error) GetUserByUsername(ctx context.Context, username string) (*User, error) GetAllUsers(ctx context.Context) ([]*User, error) - UserExists(ctx context.Context, email string) (bool, error) + UserExists(ctx context.Context, username string) (bool, error) } type PGUserStore struct {