From b7110ee008fe1c976057c765616cd1a54c935297 Mon Sep 17 00:00:00 2001 From: Adelina Simion <43963729+addetz@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:44:59 +0200 Subject: [PATCH] feat: add spacetastic endpoints DOC-1138 (#16) * feat: add page routes to counter DOC-1138 * feat: add spacetastic endpoints DOC-1138 * docs: adjust readme DOC-1138 * docs: update tests and db version * docs: update postman collection * docs: add testcontainers to counter test * chore: Updated coverage badge. * docs: update Go version in Dockerfile * docs: bump db version in makefile * docs: remove println * docs: adjust assertions * docs: fix format * docs: fix README * Apply suggestions from code review Co-authored-by: Karl Cardenas <29551334+karl-cardenas-coding@users.noreply.github.com> --------- Co-authored-by: GitHub Action Co-authored-by: Karl Cardenas <29551334+karl-cardenas-coding@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- Dockerfile | 2 +- Makefile | 2 +- README.md | 33 +++++- endpoints/counterRoute.go | 60 +++++++--- endpoints/counterRoute_test.go | 207 ++++++++++++++++++++++++--------- endpoints/healthRoute.go | 4 +- endpoints/healthRoute_test.go | 22 ++-- endpoints/helpers_test.go | 78 +++++++++++++ endpoints/types.go | 11 +- go.mod | 40 ++++--- go.sum | 45 +++++++ internal/database.go | 1 + internal/database_test.go | 44 ++++--- main.go | 4 +- tests/postman_collection.json | 79 ++++++++++++- 16 files changed, 492 insertions(+), 142 deletions(-) create mode 100644 endpoints/helpers_test.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bfcfb4b..869672f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,7 +7,7 @@ on: - main env: - DB_VERSION: 1.0.0 + DB_VERSION: 1.1.0 concurrency: group: ci-${{ github.ref }} diff --git a/Dockerfile b/Dockerfile index 373e7ed..fc72b60 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Copyright (c) Spectro Cloud # SPDX-License-Identifier: MPL-2.0 -FROM golang:1.21.7-alpine3.19 as builder +FROM golang:1.23.0-alpine3.20 as builder WORKDIR /go/src/app COPY . . RUN go build -o /go/bin/app && \ diff --git a/Makefile b/Makefile index 2ea0fac..942745f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: license -VERSION:=1.0.0 +VERSION:=1.1.0 build: go build -o hello-universe-api diff --git a/README.md b/README.md index 95eed6d..5d6517b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) -![Coverage](https://img.shields.io/badge/Coverage-54.2%25-yellow) +![Coverage](https://img.shields.io/badge/Coverage-42.6%25-yellow) # Hello Universe API @@ -17,13 +17,40 @@ The [Hello Universe](https://github.com/spectrocloud/hello-universe) app include A Postman collection is available to help you explore the API. Review the [Postman collection](./tests/postman_collection.json) to get started. +# Prerequisites +Ensure [Docker Desktop](https://www.docker.com/products/docker-desktop/) on your local machine is available. + +- Use the following command and ensure you receive an output displaying the version number. + ```shell + docker version + ``` + +Alternatively, you can install [Podman](https://podman.io/docs/installation). + +- If you are not using a Linux operating system, create and start the Podman Machine in your local environment. Otherwise, skip this step. + ```shell + podman machine init + podman machine start + ``` +- Use the following command and ensure you receive an output displaying the installation information. + ```shell + podman info + ``` + # Usage The quickest method to start the API server locally is by using the Docker image. ```shell -docker pull ghcr.io/spectrocloud/hello-universe-api:1.0.12 -docker run -p 3000:3000 ghcr.io/spectrocloud/hello-universe-api:1.0.12 +docker pull ghcr.io/spectrocloud/hello-universe-api:1.1.0 +docker run -p 3000:3000 ghcr.io/spectrocloud/hello-universe-api:1.1.0 +``` + +If you choose Podman, you can use the following commands. + +```shell +podman pull ghcr.io/spectrocloud/hello-universe-api:1.1.0 +podman run -p 3000:3000 ghcr.io/spectrocloud/hello-universe-api:1.1.0 ``` To start the API server you must have connectivity to a Postgres instance. Use [environment variables](#environment-variables) to customize the API server start parameters. diff --git a/endpoints/counterRoute.go b/endpoints/counterRoute.go index 4c968c4..4ac3d7a 100644 --- a/endpoints/counterRoute.go +++ b/endpoints/counterRoute.go @@ -22,13 +22,14 @@ func NewCounterHandlerContext(db *sqlx.DB, ctx context.Context, authorization bo } func (route *CounterRoute) CounterHTTPHandler(writer http.ResponseWriter, request *http.Request) { - log.Debug().Msg("POST request received. Incrementing counter.") + page := request.PathValue("page") + writer.Header().Set("Content-Type", "application/json") writer.Header().Set("Access-Control-Allow-Origin", "*") writer.Header().Set("Access-Control-Allow-Headers", "*") var payload []byte - if route.authorization && request.Method != "OPTIONS" { + if route.Authorization && request.Method != "OPTIONS" { validation := internal.ValidateToken(request.Header.Get("Authorization")) if !validation { log.Info().Msg("Invalid token.") @@ -39,7 +40,8 @@ func (route *CounterRoute) CounterHTTPHandler(writer http.ResponseWriter, reques switch request.Method { case "POST": - value, err := route.postHandler(request) + log.Debug().Msg("POST request received. Incrementing counter.") + value, err := route.postHandler(request, page) if err != nil { log.Debug().Msg("Error incrementing counter.") http.Error(writer, "Error incrementing counter.", http.StatusInternalServerError) @@ -47,7 +49,7 @@ func (route *CounterRoute) CounterHTTPHandler(writer http.ResponseWriter, reques writer.WriteHeader(http.StatusCreated) payload = value case "GET": - value, err := route.getHandler(request) + value, err := route.getHandler(page) if err != nil { log.Debug().Msg("Error getting counter value.") http.Error(writer, "Error getting counter value.", http.StatusInternalServerError) @@ -70,27 +72,27 @@ func (route *CounterRoute) CounterHTTPHandler(writer http.ResponseWriter, reques } // postHandler increments the counter in the database. -func (route *CounterRoute) postHandler(r *http.Request) ([]byte, error) { +func (route *CounterRoute) postHandler(r *http.Request, page string) ([]byte, error) { currentTime := time.Now().UTC() ua := useragent.Parse(r.UserAgent()) browser := ua.Name os := ua.OS - transaction, err := route.DB.BeginTx(route.ctx, nil) + transaction, err := route.DB.BeginTx(route.Ctx, nil) if err != nil { log.Error().Err(err).Msg("Error beginning transaction.") return []byte{}, err } - sqlQuery := `INSERT INTO counter(date,browser,os) VALUES ($1, $2, $3)` - _, err = transaction.ExecContext(route.ctx, sqlQuery, currentTime, browser, os) + sqlQuery := `INSERT INTO counter(page, date, browser, os) VALUES ($1, $2, $3, $4)` + _, err = transaction.ExecContext(route.Ctx, sqlQuery, page, currentTime, browser, os) if err != nil { log.Error().Err(err).Msg("Error inserting counter value.") log.Debug().Msgf("SQL query: %s", sqlQuery) return []byte{}, err } log.Info().Msg("Counter incremented in database.") - getNewCountQuery := `SELECT COUNT(*) AS total FROM counter` + getNewCountQuery := `SELECT COUNT(*) AS total FROM counter WHERE page = $1` var databaseTotal sql.NullInt64 - result := transaction.QueryRowContext(route.ctx, getNewCountQuery) + result := transaction.QueryRowContext(route.Ctx, getNewCountQuery, page) err = result.Scan(&databaseTotal) if err != nil { log.Error().Err(err).Msg("Error scanning counter value.") @@ -100,7 +102,7 @@ func (route *CounterRoute) postHandler(r *http.Request) ([]byte, error) { log.Error().Err(err).Msg("Counter value is null.") return []byte{}, err } - counterSummary := counterSummary{Total: databaseTotal.Int64} + counterSummary := CounterSummary{Total: databaseTotal.Int64} err = transaction.Commit() if err != nil { log.Error().Err(err).Msg("Error committing transaction.") @@ -120,13 +122,22 @@ func (route *CounterRoute) postHandler(r *http.Request) ([]byte, error) { } // getHandler returns the current counter value from the database as a JSON object. -func (route *CounterRoute) getHandler(r *http.Request) ([]byte, error) { +func (route *CounterRoute) getHandler(page string) ([]byte, error) { + if page != "" { + return route.getHandlerForPage(page) + } + + return route.getHandlerAllPages() +} + +// getHandlerAllPages returns the current counter value for all pages from the database as a JSON object. +func (route *CounterRoute) getHandlerAllPages() ([]byte, error) { sqlQuery := `SELECT COUNT(*) AS total FROM counter` - var counterSummary counterSummary - err := route.DB.GetContext(route.ctx, &counterSummary, sqlQuery) + var counterSummary CounterSummary + err := route.DB.GetContext(route.Ctx, &counterSummary, sqlQuery) if err != nil { log.Error().Err(err).Msg("Error getting counter value.") - log.Debug().Msgf("SQL query: %s", sqlQuery) + log.Info().Msgf("SQL query: %s", sqlQuery) return []byte{}, err } log.Info().Msg("Counter value retrieved from database.") @@ -137,3 +148,22 @@ func (route *CounterRoute) getHandler(r *http.Request) ([]byte, error) { } return payload, nil } + +// getHandlerForPage returns the current counter value for a single page from the database as a JSON object. +func (route *CounterRoute) getHandlerForPage(page string) ([]byte, error) { + sqlQuery := `SELECT COUNT(*) AS total FROM counter WHERE page = $1` + var counterSummary CounterSummary + err := route.DB.GetContext(route.Ctx, &counterSummary, sqlQuery, page) + if err != nil { + log.Error().Err(err).Msg("Error getting counter value.") + log.Info().Msgf("SQL query: %s", sqlQuery) + return []byte{}, err + } + log.Info().Msgf("Counter value retrieved from database for page %s", page) + payload, err := json.MarshalIndent(counterSummary, "", " ") + if err != nil { + log.Error().Err(err).Msg("Error marshalling counterSummary struct into JSON.") + return []byte{}, err + } + return payload, nil +} diff --git a/endpoints/counterRoute_test.go b/endpoints/counterRoute_test.go index c2b6f4a..02083be 100644 --- a/endpoints/counterRoute_test.go +++ b/endpoints/counterRoute_test.go @@ -1,7 +1,7 @@ // Copyright (c) Spectro Cloud // SPDX-License-Identifier: MPL-2.0 -package endpoints +package endpoints_test import ( "context" @@ -12,85 +12,68 @@ import ( "testing" "time" - "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "github.com/rs/zerolog/log" + "spectrocloud.com/hello-universe-api/endpoints" ) -// startDB returns a new database connection to the counter database. -// A local database is required to run the tests. -func startDB() (*sqlx.DB, error) { - dbUser := "postgres" - dbPassword := "password" - dbName := "counter" - host := "localhost" - dbEncryption := "disable" - - db, err := sqlx.Open("postgres", fmt.Sprintf( - "host=%s port=%d dbname=%s user=%s password=%s connect_timeout=5 sslmode=%s", - host, - 5432, - dbName, - dbUser, - dbPassword, - dbEncryption, - )) - if err != nil { - return nil, err - } - - err = db.Ping() +func TestNewCounterHandlerContext(t *testing.T) { + container, err := CreatePostgresTestContainer() if err != nil { - return nil, err + t.Errorf("Error creating PostgresTestContainer: %s", err) } - return db, err - -} - -func TestNewCounterHandlerContext(t *testing.T) { - - db, err := startDB() + db, err := StartTestDB(container) if err != nil { t.Errorf("Expected a new database connection, but got %s", err) } + defer CleanUpTestContainer(container) + ctx := context.Background() authorization := true - counter := NewCounterHandlerContext(db, ctx, authorization) + counter := endpoints.NewCounterHandlerContext(db, ctx, authorization) if counter == nil { t.Errorf("Expected a new CounterRoute, but got nil") } if counter != nil { - if counter.ctx != ctx { - t.Errorf("Expected context to be %v, but got %v", ctx, counter.ctx) + if counter.Ctx != ctx { + t.Errorf("Expected context to be %v, but got %v", ctx, counter.Ctx) } - if counter.authorization != authorization { - t.Errorf("Expected authorization to be %v, but got %v", authorization, counter.authorization) + if counter.Authorization != authorization { + t.Errorf("Expected authorization to be %v, but got %v", authorization, counter.Authorization) } } } -func TestCounterHTTPHandlerGET(t *testing.T) { +func TestCounterHTTPHandlerGETAllPages(t *testing.T) { + + page := "test" + container, err := CreatePostgresTestContainer() + if err != nil { + t.Errorf("Error creating PostgresTestContainer: %s", err) + } - db, err := startDB() + db, err := StartTestDB(container) if err != nil { t.Errorf("Expected a new database connection, but got %s", err) } - sqlQuery := `INSERT INTO counter(date,browser,os) VALUES ($1, $2, $3)` - _, err = db.Exec(sqlQuery, time.Now(), "Chrome", "Windows") + defer CleanUpTestContainer(container) + + sqlQuery := `INSERT INTO counter(page,date,browser,os) VALUES ($1, $2, $3, $4)` + _, err = db.Exec(sqlQuery, page, time.Now(), "Chrome", "Windows") if err != nil { t.Errorf("Error inserting into counter table: %s", err) } - counter := NewCounterHandlerContext(db, context.Background(), false) + counter := endpoints.NewCounterHandlerContext(db, context.Background(), false) rr := httptest.NewRecorder() req, err := http.NewRequest("GET", "v1/counter", nil) @@ -106,35 +89,93 @@ func TestCounterHTTPHandlerGET(t *testing.T) { status, http.StatusOK) } - var result counterSummary + var result endpoints.CounterSummary err = json.Unmarshal(rr.Body.Bytes(), &result) if err != nil { t.Errorf("Error unmarshalling response: %s", err) } - fmt.Println(result) + if result.Total != 1 { + t.Errorf("handler returned unexpected body: got %v want %d", + result.Total, 1) + } +} + +func TestCounterHTTPHandlerGETOnePage(t *testing.T) { - if result.Total == 0 { - t.Errorf("handler returned unexpected body: got %v want %v", - result.Total, 0) + page := "test" + container, err := CreatePostgresTestContainer() + if err != nil { + t.Errorf("Error creating PostgresTestContainer: %s", err) + } + + db, err := StartTestDB(container) + if err != nil { + t.Errorf("Expected a new database connection, but got %s", err) + } + + defer CleanUpTestContainer(container) + + sqlQuery := `INSERT INTO counter(page,date,browser,os) VALUES ($1, $2, $3, $4)` + _, err = db.Exec(sqlQuery, page, time.Now(), "Chrome", "Windows") + if err != nil { + t.Errorf("Error inserting into counter table: %s", err) + } + + counter := endpoints.NewCounterHandlerContext(db, context.Background(), false) + + rr := httptest.NewRecorder() + req, err := http.NewRequest("GET", fmt.Sprintf("v1/counter/%s", page), nil) + if err != nil { + t.Fatal(err) + } + req.SetPathValue("page", page) + + handler := http.HandlerFunc(counter.CounterHTTPHandler) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + var result endpoints.CounterSummary + + err = json.Unmarshal(rr.Body.Bytes(), &result) + if err != nil { + t.Errorf("Error unmarshalling response: %s", err) + } + + if result.Total != 1 { + t.Errorf("handler returned unexpected body: got %v want %d", + result.Total, 1) } } func TestCounterHTTPHandlerPOST(t *testing.T) { - db, err := startDB() + page := "test" + container, err := CreatePostgresTestContainer() + if err != nil { + t.Errorf("Error creating PostgresTestContainer: %s", err) + } + + db, err := StartTestDB(container) if err != nil { t.Errorf("Expected a new database connection, but got %s", err) } - counter := NewCounterHandlerContext(db, context.Background(), false) + defer CleanUpTestContainer(container) + + counter := endpoints.NewCounterHandlerContext(db, context.Background(), false) rr := httptest.NewRecorder() - req, err := http.NewRequest("POST", "v1/counter", nil) + req, err := http.NewRequest("POST", fmt.Sprintf("v1/counter/%s", page), nil) if err != nil { t.Fatal(err) } + req.SetPathValue("page", page) handler := http.HandlerFunc(counter.CounterHTTPHandler) handler.ServeHTTP(rr, req) @@ -144,7 +185,7 @@ func TestCounterHTTPHandlerPOST(t *testing.T) { status, http.StatusCreated) } - var result counterSummary + var result endpoints.CounterSummary err = json.Unmarshal(rr.Body.Bytes(), &result) if err != nil { @@ -156,17 +197,71 @@ func TestCounterHTTPHandlerPOST(t *testing.T) { result.Total, "larger than zero") } + sqlQuery := `SELECT COUNT(*) AS total FROM counter WHERE page = $1` + var counterSummary endpoints.CounterSummary + err = db.GetContext(context.Background(), &counterSummary, sqlQuery, page) + if err != nil { + log.Error().Err(err).Msg("Error getting counter value.") + log.Debug().Msgf("SQL query: %s", sqlQuery) + } + + if counterSummary.Total != 1 { + t.Errorf("handler returned unexpected body: got %v want %d", + counterSummary.Total, 1) + } +} + +func TestCounterHTTPHandlerPOSTNoPage(t *testing.T) { + + container, err := CreatePostgresTestContainer() + if err != nil { + t.Errorf("Error creating PostgresTestContainer: %s", err) + } + + db, err := StartTestDB(container) + if err != nil { + t.Errorf("Expected a new database connection, but got %s", err) + } + + defer CleanUpTestContainer(container) + + counter := endpoints.NewCounterHandlerContext(db, context.Background(), false) + + rr := httptest.NewRecorder() + req, err := http.NewRequest("POST", "v1/counter", nil) + if err != nil { + t.Fatal(err) + } + + handler := http.HandlerFunc(counter.CounterHTTPHandler) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusCreated { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusCreated) + } + + var result endpoints.CounterSummary + err = json.Unmarshal(rr.Body.Bytes(), &result) + if err != nil { + t.Errorf("Error unmarshalling response: %s", err) + } + + if result.Total != 1 { + t.Errorf("handler total returned unexpected body: got %v want %d", + result.Total, 1) + } sqlQuery := `SELECT COUNT(*) AS total FROM counter` - var counterSummary counterSummary + var counterSummary endpoints.CounterSummary err = db.GetContext(context.Background(), &counterSummary, sqlQuery) if err != nil { log.Error().Err(err).Msg("Error getting counter value.") log.Debug().Msgf("SQL query: %s", sqlQuery) } - if counterSummary.Total < 1 { - t.Errorf("handler returned unexpected body: got %v want %s", - counterSummary.Total, "larger than zero") + if counterSummary.Total != 1 { + t.Errorf("handler returned unexpected body: got %v want %d", + counterSummary.Total, 1) } } diff --git a/endpoints/healthRoute.go b/endpoints/healthRoute.go index 140ead1..dc80ecc 100644 --- a/endpoints/healthRoute.go +++ b/endpoints/healthRoute.go @@ -24,7 +24,7 @@ func (health *HealthRoute) HealthHTTPHandler(writer http.ResponseWriter, request switch request.Method { case "GET": - value, err := health.getHandler(request) + value, err := health.getHandler() if err != nil { log.Debug().Msg("Error getting counter value.") http.Error(writer, "Error getting counter value.", http.StatusInternalServerError) @@ -42,6 +42,6 @@ func (health *HealthRoute) HealthHTTPHandler(writer http.ResponseWriter, request } // getHandler returns a health check response. -func (health *HealthRoute) getHandler(r *http.Request) ([]byte, error) { +func (health *HealthRoute) getHandler() ([]byte, error) { return json.Marshal(map[string]string{"status": "OK"}) } diff --git a/endpoints/healthRoute_test.go b/endpoints/healthRoute_test.go index cf66782..7394c79 100644 --- a/endpoints/healthRoute_test.go +++ b/endpoints/healthRoute_test.go @@ -1,7 +1,7 @@ // Copyright (c) Spectro Cloud // SPDX-License-Identifier: MPL-2.0 -package endpoints +package endpoints_test import ( "context" @@ -9,24 +9,26 @@ import ( "net/http/httptest" "strings" "testing" + + "spectrocloud.com/hello-universe-api/endpoints" ) func TestNewHealthHanderContext(t *testing.T) { ctx := context.Background() authorization := true - health := NewHealthHandlerContext(ctx, authorization) + health := endpoints.NewHealthHandlerContext(ctx, authorization) if health == nil { t.Errorf("Expected a new HealthRoute, but got nil") } if health != nil { - if health.ctx == nil { - t.Errorf("Expected context to be %v, but got %v", ctx, health.ctx) + if health.Ctx == nil { + t.Errorf("Expected context to be %v, but got %v", ctx, health.Ctx) } - if health.authorization != authorization { - t.Errorf("Expected authorization to be %v, but got %v", authorization, health.authorization) + if health.Authorization != authorization { + t.Errorf("Expected authorization to be %v, but got %v", authorization, health.Authorization) } } @@ -35,7 +37,7 @@ func TestNewHealthHanderContext(t *testing.T) { func TestHealthHTTPHandler(t *testing.T) { - health := NewHealthHandlerContext(context.Background(), false) + health := endpoints.NewHealthHandlerContext(context.Background(), false) rr := httptest.NewRecorder() req, err := http.NewRequest("GET", "v1/health", nil) @@ -61,7 +63,7 @@ func TestHealthHTTPHandler(t *testing.T) { func TestHealthHTTPHandlerInvalidMethod(t *testing.T) { - health := NewHealthHandlerContext(context.Background(), false) + health := endpoints.NewHealthHandlerContext(context.Background(), false) rr := httptest.NewRecorder() req, err := http.NewRequest("POST", "v1/health", nil) @@ -88,7 +90,7 @@ func TestHealthHTTPHandlerInvalidMethod(t *testing.T) { func TestHealthHTTPHandlerInvalidToken(t *testing.T) { - health := NewHealthHandlerContext(context.Background(), true) + health := endpoints.NewHealthHandlerContext(context.Background(), true) rr := httptest.NewRecorder() req, err := http.NewRequest("GET", "v1/health", nil) @@ -115,7 +117,7 @@ func TestHealthHTTPHandlerInvalidToken(t *testing.T) { func TestHealthHTTPHandlerValidToken(t *testing.T) { - health := NewHealthHandlerContext(context.Background(), true) + health := endpoints.NewHealthHandlerContext(context.Background(), true) rr := httptest.NewRecorder() req, err := http.NewRequest("GET", "v1/health", nil) diff --git a/endpoints/helpers_test.go b/endpoints/helpers_test.go new file mode 100644 index 0000000..5485a15 --- /dev/null +++ b/endpoints/helpers_test.go @@ -0,0 +1,78 @@ +package endpoints_test + +import ( + "context" + "fmt" + "time" + + "github.com/jmoiron/sqlx" + "github.com/rs/zerolog/log" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" + "spectrocloud.com/hello-universe-api/internal" +) + +const ( + dbName string = "counter" + dbUser string = "postgres" + dbPassword string = "password" + image string = "ghcr.io/spectrocloud/hello-universe-db" + image_version string = "1.1.0" +) + +func CreatePostgresTestContainer() (*postgres.PostgresContainer, error) { + ctx := context.Background() + postgresContainer, err := postgres.Run(ctx, fmt.Sprintf("%s:%s", image, image_version), + postgres.WithDatabase(dbName), + postgres.WithUsername(dbUser), + postgres.WithPassword(dbPassword), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(5*time.Second)), + ) + if err != nil { + return nil, err + } + + return postgresContainer, nil +} + +// StartTestDB returns a new database connection to the counter database. +func StartTestDB(container *postgres.PostgresContainer) (*sqlx.DB, error) { + ctx := context.Background() + connection, err := container.ConnectionString(ctx, + fmt.Sprintf("user=%s", dbUser), + fmt.Sprintf("password=%s", dbPassword), + fmt.Sprintf("dbname=%s", dbName), + "sslmode=disable", + ) + if err != nil { + return nil, fmt.Errorf("failed to get container connection string: %s", err) + } + + db, err := sqlx.Open("postgres", connection) + if err != nil { + return nil, err + } + + err = db.Ping() + if err != nil { + return nil, err + } + + err = internal.InitDB(ctx, db) + if err != nil { + return nil, fmt.Errorf("Expected database initialization, but got %s", err) + } + + return db, err +} + +func CleanUpTestContainer(container *postgres.PostgresContainer) { + ctx := context.Background() + if err := container.Terminate(ctx); err != nil { + log.Fatal().Msgf("failed to terminate container: %s", err) + } +} diff --git a/endpoints/types.go b/endpoints/types.go index 0f72355..b2a3be2 100644 --- a/endpoints/types.go +++ b/endpoints/types.go @@ -12,6 +12,7 @@ import ( type Counter struct { Id int `json:"id" db:"id"` + Page string `json:"page" db:"page"` Date *sql.NullTime `json:"date" db:"date"` Browser string `json:"browser" db:"browser"` Os string `json:"os" db:"os"` @@ -19,16 +20,16 @@ type Counter struct { type CounterRoute struct { DB *sqlx.DB - ctx context.Context - authorization bool + Ctx context.Context + Authorization bool } -type counterSummary struct { +type CounterSummary struct { Total int64 `json:"total" db:"total"` Counts []Counter `json:"counts,omitempty" db:"counts"` } type HealthRoute struct { - ctx context.Context - authorization bool + Ctx context.Context + Authorization bool } diff --git a/go.mod b/go.mod index 8ff2363..15acfce 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,29 @@ module spectrocloud.com/hello-universe-api -go 1.21 +go 1.23 require ( github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.9 github.com/mileusna/useragent v1.3.4 github.com/rs/zerolog v1.32.0 - github.com/testcontainers/testcontainers-go v0.28.0 - github.com/testcontainers/testcontainers-go/modules/postgres v0.28.0 + github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0 go.uber.org/automaxprocs v1.5.3 ) require ( dario.cat/mergo v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.11.5 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/containerd/containerd v1.7.13 // indirect + github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect - github.com/distribution/reference v0.5.0 // indirect - github.com/docker/docker v25.0.3+incompatible // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -30,13 +31,14 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.7 // indirect github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/sys/user v0.1.0 // indirect @@ -52,17 +54,17 @@ require ( github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect - go.opentelemetry.io/otel v1.23.1 // indirect - go.opentelemetry.io/otel/metric v1.23.1 // indirect - go.opentelemetry.io/otel/trace v1.23.1 // indirect - golang.org/x/crypto v0.19.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/mod v0.15.0 // indirect - golang.org/x/sys v0.17.0 // indirect + golang.org/x/sys v0.21.0 // indirect golang.org/x/tools v0.18.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect - google.golang.org/grpc v1.62.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.64.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect ) diff --git a/go.sum b/go.sum index 880c0a3..c434436 100644 --- a/go.sum +++ b/go.sum @@ -6,14 +6,22 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= +github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/containerd v1.7.13 h1:wPYKIeGMN8vaggSKuV1X0wZulpMz4CrgEsZdaCyB6Is= github.com/containerd/containerd v1.7.13/go.mod h1:zT3up6yTRfEUa6+GsITYIJNgSVL9NQ4x4h1RPzk0Wu4= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= @@ -24,8 +32,12 @@ 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= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v25.0.3+incompatible h1:D5fy/lYmY7bvZa0XTZ5/UJPljor41F+vdyJG5luQLfQ= github.com/docker/docker v25.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -48,6 +60,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -63,6 +77,7 @@ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -87,6 +102,8 @@ github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRU github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mileusna/useragent v1.3.4 h1:MiuRRuvGjEie1+yZHO88UBYg8YBC/ddF6T7F56i3PCk= github.com/mileusna/useragent v1.3.4/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= @@ -129,10 +146,15 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/testcontainers/testcontainers-go v0.28.0 h1:1HLm9qm+J5VikzFDYhOd+Zw12NtOl+8drH2E8nTY1r8= github.com/testcontainers/testcontainers-go v0.28.0/go.mod h1:COlDpUXbwW3owtpMkEB1zo9gwb1CoKVKlyrVPejF4AU= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= github.com/testcontainers/testcontainers-go/modules/postgres v0.28.0 h1:ff0s4JdYIdNAVSi/SrpN2Pdt1f+IjIw3AKjbHau8Un4= github.com/testcontainers/testcontainers-go/modules/postgres v0.28.0/go.mod h1:fXgcYpbyrduNdiz2qRZuYkmvqLnEqsjbQiBNYH1ystI= +github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0 h1:c+Gt+XLJjqFAejgX4hSpnHIpC9eAhvgI/TFWL/PbrFI= +github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0/go.mod h1:I4DazHBoWDyf69ByOIyt3OdNjefiUx372459txOpQ3o= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= @@ -146,18 +168,26 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY= go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo= go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8= go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= @@ -167,6 +197,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -179,6 +211,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -200,10 +233,13 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -218,17 +254,26 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A= google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= diff --git a/internal/database.go b/internal/database.go index 41e5dc5..38718c6 100644 --- a/internal/database.go +++ b/internal/database.go @@ -17,6 +17,7 @@ func InitDB(ctx context.Context, db *sqlx.DB) error { sqlStatement := ` CREATE TABLE IF NOT EXISTS counter ( id SERIAL PRIMARY KEY, + page varchar(255), date timestamp NOT NULL, browser varchar(255), os varchar(255) diff --git a/internal/database_test.go b/internal/database_test.go index d3f111d..4be23a8 100644 --- a/internal/database_test.go +++ b/internal/database_test.go @@ -1,4 +1,4 @@ -package internal +package internal_test import ( "context" @@ -12,23 +12,21 @@ import ( "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" + "spectrocloud.com/hello-universe-api/internal" +) + +const ( + dbName string = "counter" + dbUser string = "postgres" + dbPassword string = "password" + image string = "ghcr.io/spectrocloud/hello-universe-db" + image_version string = "1.1.0" ) func TestInitDB(t *testing.T) { ctx := context.Background() - - dbName := "counter" - dbUser := "postgres" - dbPassword := "password" - - const ( - image string = "ghcr.io/spectrocloud/hello-universe-db" - image_veresion string = "1.0.0" - ) - - postgresContainer, err := postgres.RunContainer(ctx, - testcontainers.WithImage(image+":"+image_veresion), + postgresContainer, err := postgres.Run(ctx, fmt.Sprintf("%s:%s", image, image_version), postgres.WithDatabase(dbName), postgres.WithUsername(dbUser), postgres.WithPassword(dbPassword), @@ -46,9 +44,9 @@ func TestInitDB(t *testing.T) { log.Fatalf("failed to start database: %s", err) } - err = InitDB(ctx, db) + err = internal.InitDB(ctx, db) if err != nil { - t.Errorf("Expected database initailization, but got %s", err) + t.Errorf("Expected database initialization, but got %s", err) } // Check if the table was created @@ -62,15 +60,13 @@ func TestInitDB(t *testing.T) { var tableExists bool err = db.QueryRowContext(ctx, query).Scan(&tableExists) if err != nil { - t.Errorf("Unable to query the database: %s", err) + t.Errorf("Unable to query the database: %s", err) } - - + if !tableExists { - t.Errorf("Expected table 'counter' to exist, but it does not.") + t.Errorf("Expected table 'counter' to exist, but it does not.") } - defer func() { if err := postgresContainer.Terminate(ctx); err != nil { log.Fatalf("failed to terminate container: %s", err) @@ -81,10 +77,10 @@ func TestInitDB(t *testing.T) { func startDB(ctx context.Context, container *postgres.PostgresContainer) (*sqlx.DB, error) { - connection, err := container.ConnectionString(ctx, - "user=posgres", - "password=password", - "dbname=counter", + connection, err := container.ConnectionString(ctx, + fmt.Sprintf("user=%s", dbUser), + fmt.Sprintf("password=%s", dbPassword), + fmt.Sprintf("dbname=%s", dbName), "sslmode=disable", ) if err != nil { diff --git a/main.go b/main.go index 6698083..718895c 100644 --- a/main.go +++ b/main.go @@ -101,13 +101,15 @@ func main() { healthRoute := endpoints.NewHealthHandlerContext(ctx, globalAuthorization) http.HandleFunc(internal.ApiPrefix+"counter", counterRoute.CounterHTTPHandler) + http.HandleFunc(internal.ApiPrefix+"counter/{page}", counterRoute.CounterHTTPHandler) http.HandleFunc(internal.ApiPrefix+"health", healthRoute.HealthHTTPHandler) - log.Info().Msgf("Server is configured for port %s and listing on %s", globalPort, globalHostURL) + log.Info().Msgf("Server is configured for port %s and listening on %s", globalPort, globalHostURL) log.Info().Msgf("Database is configured for %s:%d", dbHost, dbPort) log.Info().Msgf("Trace level set to: %s", globalTraceLevel) log.Info().Msgf("Authorization is set to: %v", globalAuthorization) log.Info().Msg("Starting server...") + err := http.ListenAndServe(globalHostURL, nil) if err != nil { log.Fatal().Err(err).Msg("There's an error with the server") diff --git a/tests/postman_collection.json b/tests/postman_collection.json index 23b7dc3..b844289 100644 --- a/tests/postman_collection.json +++ b/tests/postman_collection.json @@ -1,15 +1,16 @@ { "info": { - "_postman_id": "c699c8f4-1a1e-4da9-80fc-11597c6400cc", + "_postman_id": "0741b219-d856-413d-8490-ede93f24c4fe", "name": "Hello Universe API", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "38152584" }, "item": [ { "name": "Counter", "item": [ { - "name": "Increase Click Counter", + "name": "Add New Event", "event": [ { "listen": "test", @@ -43,7 +44,41 @@ "response": [] }, { - "name": "Get Click Counter", + "name": "Add New Event for Page", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:3000/api/v1/counter", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "v1", + "counter" + ] + } + }, + "response": [] + }, + { + "name": "Get Counter for All Pages", "event": [ { "listen": "test", @@ -75,6 +110,42 @@ } }, "response": [] + }, + { + "name": "Get Counter for Page", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/api/v1/counter/testPage", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "v1", + "counter", + "testPage" + ] + } + }, + "response": [] } ] },