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}