Skip to content

Commit

Permalink
test: endpoint handler
Browse files Browse the repository at this point in the history
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
  • Loading branch information
crazybolillo committed Aug 25, 2024
1 parent 0b95fed commit ea05178
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 2 additions & 3 deletions internal/handler/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
218 changes: 218 additions & 0 deletions internal/handler/endpoint_test.go
Original file line number Diff line number Diff line change
@@ -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:[email protected]: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)
}
}
}

0 comments on commit ea05178

Please sign in to comment.