From ea05178fbfdea1538bff7e6a223726f0d3a70f1f Mon Sep 17 00:00:00 2001 From: crazybolillo Date: Fri, 23 Aug 2024 21:01:04 -0600 Subject: [PATCH] test: endpoint handler This commit tests all operations supported by the endpoint handler. It also establishes the structure for future tests. The tests rely on a external postgres server. A compose file has been added to quickly set up the postgres server. This was prefered over adding an extra dependency like testcontainers which also involves more code to wire containers. Any bugs found during testing have been fixed as well. k --- .github/workflows/qa.yaml | 2 + Makefile | 3 + compose.yaml | 17 +++ internal/handler/endpoint.go | 5 +- internal/handler/endpoint_test.go | 218 ++++++++++++++++++++++++++++++ 5 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 compose.yaml diff --git a/.github/workflows/qa.yaml b/.github/workflows/qa.yaml index 6544651..236e38b 100644 --- a/.github/workflows/qa.yaml +++ b/.github/workflows/qa.yaml @@ -20,6 +20,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 + - name: Start Postgresql + run: make db - name: Run tests run: go test -v ./... swagger: diff --git a/Makefile b/Makefile index a43b797..e7ecf35 100644 --- a/Makefile +++ b/Makefile @@ -5,3 +5,6 @@ docs: swagger: docker run --detach --name eryth-swagger -p 4000:8080 -e API_URL=/doc/swagger.yaml --mount 'type=bind,src=$(shell pwd)/docs,dst=/usr/share/nginx/html/doc' swaggerapi/swagger-ui + +db: + docker compose up --wait db \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..f03e43f --- /dev/null +++ b/compose.yaml @@ -0,0 +1,17 @@ +name: eryth +services: + db: + image: postgres:15-alpine + ports: + - '54321:5432' + environment: + - POSTGRES_USER=go + - POSTGRES_PASSWORD=go + - POSTGRES_DB=eryth + volumes: + - ./db/migrations/:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U go" ] + interval: 1s + timeout: 1s + retries: 10 \ No newline at end of file diff --git a/internal/handler/endpoint.go b/internal/handler/endpoint.go index ec69491..93dc753 100644 --- a/internal/handler/endpoint.go +++ b/internal/handler/endpoint.go @@ -112,7 +112,6 @@ func (e *Endpoint) list(w http.ResponseWriter, r *http.Request) { if err != nil { slog.Error("Failed to marshall endpoint list", slog.String("path", r.URL.Path), slog.String("reason", err.Error())) } - w.WriteHeader(http.StatusOK) } // @Summary Create a new endpoint. @@ -149,12 +148,13 @@ func (e *Endpoint) create(w http.ResponseWriter, r *http.Request) { return } + w.WriteHeader(http.StatusCreated) w.Header().Set("Content-Type", "application/json") _, err = w.Write(content) if err != nil { slog.Error("Failed to write response", slog.String("path", r.URL.Path), slog.String("reason", err.Error())) } - w.WriteHeader(http.StatusCreated) + } // @Summary Delete an endpoint and its associated resources. @@ -225,5 +225,4 @@ func (e *Endpoint) update(w http.ResponseWriter, r *http.Request) { if err != nil { slog.Error("Failed to write response", slog.String("path", r.URL.Path), slog.String("reason", err.Error())) } - w.WriteHeader(http.StatusOK) } diff --git a/internal/handler/endpoint_test.go b/internal/handler/endpoint_test.go index abeebd1..b4f11f8 100644 --- a/internal/handler/endpoint_test.go +++ b/internal/handler/endpoint_test.go @@ -1 +1,219 @@ package handler + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/crazybolillo/eryth/internal/model" + "github.com/crazybolillo/eryth/internal/service" + "github.com/jackc/pgx/v5" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestEndpointAPI(t *testing.T) { + cases := []struct { + name string + test func(handler http.Handler) func(*testing.T) + }{ + {"Create", MustCreate}, + {"Delete", MustDelete}, + {"Read", MustRead}, + {"Update", MustUpdate}, + } + + conn, err := pgx.Connect(context.Background(), "postgres://go:go@127.0.0.1:54321/eryth") + if err != nil { + t.Fatalf( + "Connection to test database failed: %s. Try running 'make db' and run the tests again", + err, + ) + } + defer func(conn *pgx.Conn, ctx context.Context) { + err := conn.Close(ctx) + if err != nil { + t.Error("Failed to close db connection") + } + }(conn, context.Background()) + + for _, tt := range cases { + tx, err := conn.Begin(context.Background()) + if err != nil { + t.Fatalf("Transaction start failed: %s", err) + } + + handler := Endpoint{Service: &service.EndpointService{Cursor: tx}} + t.Run(tt.name, tt.test(handler.Router())) + + err = tx.Rollback(context.Background()) + if err != nil { + t.Fatalf("Failed to rollback transaction: %s", err) + } + } +} + +func createEndpoint(t *testing.T, handler http.Handler, endpoint model.NewEndpoint) *httptest.ResponseRecorder { + payload, err := json.Marshal(endpoint) + if err != nil { + t.Errorf("failed to marshal new endpoint: %s", err) + } + + req := httptest.NewRequest("POST", "/", bytes.NewReader(payload)) + req.Header.Set("Content-Type", "application/json") + res := httptest.NewRecorder() + handler.ServeHTTP(res, req) + + return res +} + +func readEndpoint(handler http.Handler, sid int32) *httptest.ResponseRecorder { + req := httptest.NewRequest("GET", fmt.Sprintf("/%d", sid), nil) + res := httptest.NewRecorder() + handler.ServeHTTP(res, req) + + return res +} + +func updateEndpoint(t *testing.T, handler http.Handler, sid int32, endpoint model.PatchedEndpoint) *httptest.ResponseRecorder { + payload, err := json.Marshal(endpoint) + if err != nil { + t.Errorf("failed to marshal new endpoint: %s", err) + } + req := httptest.NewRequest("PATCH", fmt.Sprintf("/%d", sid), bytes.NewReader(payload)) + res := httptest.NewRecorder() + handler.ServeHTTP(res, req) + + return res +} + +func parseEndpoint(t *testing.T, content *bytes.Buffer) model.Endpoint { + var createdEndpoint model.Endpoint + decoder := json.NewDecoder(content) + err := decoder.Decode(&createdEndpoint) + if err != nil { + t.Errorf("failed to parse endpoint: %s", err) + } + + return createdEndpoint +} + +func MustCreate(handler http.Handler) func(*testing.T) { + return func(t *testing.T) { + endpoint := model.NewEndpoint{ + ID: "zinniaelegans", + Password: "verylongandsafepassword", + Context: "flowers", + Codecs: []string{"ulaw", "g722"}, + Extension: "1234", + DisplayName: "Zinnia Elegans", + MaxContacts: 10, + } + res := createEndpoint(t, handler, endpoint) + if res.Code != http.StatusCreated { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusCreated) + } + got := parseEndpoint(t, res.Body) + + want := model.Endpoint{ + Sid: got.Sid, + ID: endpoint.ID, + DisplayName: endpoint.DisplayName, + Transport: endpoint.Transport, + Context: endpoint.Context, + Codecs: endpoint.Codecs, + MaxContacts: 10, + Extension: "1234", + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("Created endpoint does not match request, got %v, want %v", got, want) + } + } +} + +func MustRead(handler http.Handler) func(*testing.T) { + return func(t *testing.T) { + endpoint := model.NewEndpoint{ + ID: "kiwi", + Password: "kiwipassword123", + Context: "fruits", + Codecs: nil, + Extension: "9000", + DisplayName: "Blue Kiwi", + } + res := createEndpoint(t, handler, endpoint) + want := parseEndpoint(t, res.Body) + + res = readEndpoint(handler, want.Sid) + if res.Code != http.StatusOK { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusOK) + } + got := parseEndpoint(t, res.Body) + + if !reflect.DeepEqual(want, got) { + t.Errorf("read endpoint does not match want, got %v, want %v", got, want) + } + } +} + +func MustDelete(handler http.Handler) func(*testing.T) { + return func(t *testing.T) { + endpoint := model.NewEndpoint{ + ID: "testuser", + Password: "testpassword123$", + Context: "internal", + Codecs: nil, + Extension: "4000", + DisplayName: "Mr. Test User", + } + + res := createEndpoint(t, handler, endpoint) + createdEndpoint := parseEndpoint(t, res.Body) + + req := httptest.NewRequest("DELETE", fmt.Sprintf("/%d", createdEndpoint.Sid), nil) + res = httptest.NewRecorder() + handler.ServeHTTP(res, req) + + if res.Code != http.StatusNoContent { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusNoContent) + } + + res = readEndpoint(handler, createdEndpoint.Sid) + if res.Code != http.StatusNotFound { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusNotFound) + } + } +} + +func MustUpdate(handler http.Handler) func(*testing.T) { + return func(t *testing.T) { + endpoint := model.NewEndpoint{ + ID: "big_chungus", + Password: "big_chungus_password", + Context: "memes", + Codecs: []string{"ulaw", "opus"}, + Extension: "5061", + DisplayName: "Big Chungus", + } + res := createEndpoint(t, handler, endpoint) + want := parseEndpoint(t, res.Body) + want.MaxContacts = 5 + want.Extension = "6072" + + res = updateEndpoint(t, handler, want.Sid, model.PatchedEndpoint{ + MaxContacts: &want.MaxContacts, + Extension: &want.Extension, + }) + if res.Code != http.StatusOK { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusOK) + } + got := parseEndpoint(t, res.Body) + + if !reflect.DeepEqual(got, want) { + t.Errorf("inconsistent update result, got %v, want %v", got, want) + } + } +}