helpers_test.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package lunatask_test
  6
  7import (
  8	"context"
  9	"encoding/json"
 10	"io"
 11	"net/http"
 12	"net/http/httptest"
 13	"net/url"
 14	"testing"
 15
 16	lunatask "git.secluded.site/go-lunatask"
 17)
 18
 19const testToken = "test-token"
 20
 21// clientCall is a function that calls a client method and returns an error.
 22type clientCall func(*lunatask.Client) error
 23
 24// testErrorCases runs standard error case tests (401, 404, 500) for a client method.
 25func testErrorCases(t *testing.T, call clientCall) {
 26	t.Helper()
 27
 28	cases := []struct {
 29		name   string
 30		status int
 31	}{
 32		{"unauthorized", http.StatusUnauthorized},
 33		{"not_found", http.StatusNotFound},
 34		{"server_error", http.StatusInternalServerError},
 35	}
 36
 37	for _, errorCase := range cases {
 38		t.Run(errorCase.name, func(t *testing.T) {
 39			t.Parallel()
 40
 41			server := newStatusServer(errorCase.status)
 42			defer server.Close()
 43
 44			client := lunatask.NewClient(testToken, lunatask.BaseURL(server.URL))
 45			if err := call(client); err == nil {
 46				t.Errorf("expected error for %d, got nil", errorCase.status)
 47			}
 48		})
 49	}
 50}
 51
 52// newStatusServer returns a server that responds with the given status code.
 53func newStatusServer(status int) *httptest.Server {
 54	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
 55		w.WriteHeader(status)
 56	}))
 57}
 58
 59// newJSONServer returns a server that verifies GET method, path, auth and responds with JSON.
 60func newJSONServer(t *testing.T, wantPath string, body string) *httptest.Server {
 61	t.Helper()
 62
 63	return httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
 64		if req.Method != http.MethodGet {
 65			t.Errorf("Method = %s, want GET", req.Method)
 66		}
 67
 68		if req.URL.Path != wantPath {
 69			t.Errorf("Path = %s, want %s", req.URL.Path, wantPath)
 70		}
 71
 72		if auth := req.Header.Get("Authorization"); auth != "bearer "+testToken {
 73			t.Errorf("Authorization = %q, want %q", auth, "bearer "+testToken)
 74		}
 75
 76		writer.WriteHeader(http.StatusOK)
 77
 78		if _, err := writer.Write([]byte(body)); err != nil {
 79			t.Errorf("write response: %v", err)
 80		}
 81	}))
 82}
 83
 84func ptr[T any](v T) *T {
 85	return &v
 86}
 87
 88func ctx() context.Context {
 89	return context.Background()
 90}
 91
 92// requestCapture holds captured request data from a mock server.
 93type requestCapture struct {
 94	Body map[string]any
 95}
 96
 97// newBodyServer returns a server that verifies method, path, auth, Content-Type
 98// and captures the request body.
 99func newBodyServer(
100	t *testing.T, wantMethod, wantPath, responseBody string,
101) (*httptest.Server, *requestCapture) {
102	t.Helper()
103
104	capture := &requestCapture{Body: nil}
105
106	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
107		if req.Method != wantMethod {
108			t.Errorf("Method = %s, want %s", req.Method, wantMethod)
109		}
110
111		if req.URL.Path != wantPath {
112			t.Errorf("Path = %s, want %s", req.URL.Path, wantPath)
113		}
114
115		if auth := req.Header.Get("Authorization"); auth != "bearer "+testToken {
116			t.Errorf("Authorization = %q, want %q", auth, "bearer "+testToken)
117		}
118
119		if contentType := req.Header.Get("Content-Type"); contentType != "application/json" {
120			t.Errorf("Content-Type = %q, want application/json", contentType)
121		}
122
123		body, _ := io.ReadAll(req.Body)
124		_ = json.Unmarshal(body, &capture.Body)
125
126		writer.WriteHeader(http.StatusOK)
127
128		if _, err := writer.Write([]byte(responseBody)); err != nil {
129			t.Errorf("write response: %v", err)
130		}
131	}))
132
133	return server, capture
134}
135
136// newPOSTServer returns a server that verifies POST method, path, auth and captures request body.
137func newPOSTServer(t *testing.T, wantPath, responseBody string) (*httptest.Server, *requestCapture) {
138	t.Helper()
139
140	return newBodyServer(t, http.MethodPost, wantPath, responseBody)
141}
142
143// newPUTServer returns a server that verifies PUT method, path, auth and captures request body.
144func newPUTServer(t *testing.T, wantPath, responseBody string) (*httptest.Server, *requestCapture) {
145	t.Helper()
146
147	return newBodyServer(t, http.MethodPut, wantPath, responseBody)
148}
149
150// newDELETEServer returns a server that verifies DELETE method, path, auth and responds with JSON.
151func newDELETEServer(t *testing.T, wantPath, responseBody string) *httptest.Server {
152	t.Helper()
153
154	return httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
155		if req.Method != http.MethodDelete {
156			t.Errorf("Method = %s, want DELETE", req.Method)
157		}
158
159		if req.URL.Path != wantPath {
160			t.Errorf("Path = %s, want %s", req.URL.Path, wantPath)
161		}
162
163		if auth := req.Header.Get("Authorization"); auth != "bearer "+testToken {
164			t.Errorf("Authorization = %q, want %q", auth, "bearer "+testToken)
165		}
166
167		writer.WriteHeader(http.StatusOK)
168
169		if _, err := writer.Write([]byte(responseBody)); err != nil {
170			t.Errorf("write response: %v", err)
171		}
172	}))
173}
174
175// assertBodyField checks that a JSON body contains the expected string value.
176func assertBodyField(t *testing.T, body map[string]any, key, want string) {
177	t.Helper()
178
179	if body[key] != want {
180		t.Errorf("body.%s = %v, want %q", key, body[key], want)
181	}
182}
183
184// assertBodyFieldFloat checks that a JSON body contains the expected float64 value.
185// JSON unmarshals numbers to float64.
186func assertBodyFieldFloat(t *testing.T, body map[string]any, key string, want float64) {
187	t.Helper()
188
189	if body[key] != want {
190		t.Errorf("body.%s = %v, want %v", key, body[key], want)
191	}
192}
193
194// filterTest describes a test case for source filtering.
195type filterTest struct {
196	Name      string
197	Source    *string
198	SourceID  *string
199	WantQuery url.Values
200}
201
202// runFilterTests runs source filter tests against a list endpoint.
203func runFilterTests(
204	t *testing.T, path, emptyResponse string, tests []filterTest, callAPI func(*lunatask.Client, *string, *string) error,
205) {
206	t.Helper()
207
208	for _, testCase := range tests {
209		t.Run(testCase.Name, func(t *testing.T) {
210			t.Parallel()
211
212			var gotQuery url.Values
213
214			server := newJSONServer(t, path, emptyResponse)
215
216			server.Config.Handler = http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
217				gotQuery = req.URL.Query()
218
219				writer.WriteHeader(http.StatusOK)
220				_, _ = writer.Write([]byte(emptyResponse))
221			})
222			defer server.Close()
223
224			client := lunatask.NewClient(testToken, lunatask.BaseURL(server.URL))
225
226			if err := callAPI(client, testCase.Source, testCase.SourceID); err != nil {
227				t.Fatalf("error = %v", err)
228			}
229
230			for key, want := range testCase.WantQuery {
231				if got := gotQuery.Get(key); got != want[0] {
232					t.Errorf("query[%s] = %q, want %q", key, got, want[0])
233				}
234			}
235		})
236	}
237}