Skip to content

Commit 28ca3ac

Browse files
committed
Implement new Client interface in API, enhance testing with testify, and improve error handling across modules.
1 parent 409f558 commit 28ca3ac

13 files changed

Lines changed: 727 additions & 397 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ test:
1818
format:
1919
go fmt ./...
2020

21+
.PHONY: all
22+
all: build test format
23+
2124
.PHONY: release
2225
release:
2326
goreleaser release --clean

api/client.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ import (
2020
"xurl/version"
2121
)
2222

23+
// Client is an interface for API clients
24+
type Client interface {
25+
GetAuthHeader(method, url string, authType string, username string) (string, error)
26+
BuildRequest(method, endpoint string, headers []string, body io.Reader, contentType string, authType string, username string) (*http.Request, error)
27+
SendRequest(method, endpoint string, headers []string, data string, authType string, username string, verbose bool) (json.RawMessage, error)
28+
StreamRequest(method, endpoint string, headers []string, data string, authType string, username string, verbose bool) error
29+
SendMultipartRequest(method, endpoint string, headers []string, formFields map[string]string, fileField, filePath string, authType string, username string, verbose bool) (json.RawMessage, error)
30+
SendMultipartRequestWithBuffer(method, endpoint string, headers []string, formFields map[string]string, fileField, fileName string, fileData []byte, authType string, username string, verbose bool) (json.RawMessage, error)
31+
}
32+
2333
// ApiClient handles API requests
2434
type ApiClient struct {
2535
url string
@@ -129,7 +139,7 @@ func (c *ApiClient) BuildRequest(method, endpoint string, headers []string, body
129139
}
130140

131141
// processResponse handles common response processing logic
132-
func (c *ApiClient) processResponse(resp *http.Response, verbose bool) (json.RawMessage, *xurlErrors.Error) {
142+
func (c *ApiClient) processResponse(resp *http.Response, verbose bool) (json.RawMessage, error) {
133143
responseBody, err := io.ReadAll(resp.Body)
134144
if err != nil {
135145
return nil, xurlErrors.NewIOError(err)
@@ -178,7 +188,7 @@ func (c *ApiClient) logRequest(req *http.Request, verbose bool) {
178188
}
179189

180190
// SendRequest sends an HTTP request
181-
func (c *ApiClient) SendRequest(method, endpoint string, headers []string, data string, authType string, username string, verbose bool) (json.RawMessage, *xurlErrors.Error) {
191+
func (c *ApiClient) SendRequest(method, endpoint string, headers []string, data string, authType string, username string, verbose bool) (json.RawMessage, error) {
182192
var body io.Reader
183193
contentType := ""
184194

@@ -210,7 +220,7 @@ func (c *ApiClient) SendRequest(method, endpoint string, headers []string, data
210220

211221
// prepareMultipartRequest prepares a multipart request with common setup
212222
func (c *ApiClient) prepareMultipartRequest(method, endpoint string, headers []string, formFields map[string]string,
213-
writer *multipart.Writer, body *bytes.Buffer, authType string, username string) (*http.Request, *xurlErrors.Error) {
223+
writer *multipart.Writer, body *bytes.Buffer, authType string, username string) (*http.Request, error) {
214224

215225
for key, value := range formFields {
216226
if err := writer.WriteField(key, value); err != nil {
@@ -231,7 +241,7 @@ func (c *ApiClient) prepareMultipartRequest(method, endpoint string, headers []s
231241
}
232242

233243
// SendMultipartRequest sends an HTTP request with multipart form data
234-
func (c *ApiClient) SendMultipartRequest(method, endpoint string, headers []string, formFields map[string]string, fileField, filePath string, authType string, username string, verbose bool) (json.RawMessage, *xurlErrors.Error) {
244+
func (c *ApiClient) SendMultipartRequest(method, endpoint string, headers []string, formFields map[string]string, fileField, filePath string, authType string, username string, verbose bool) (json.RawMessage, error) {
235245
body := &bytes.Buffer{}
236246
writer := multipart.NewWriter(body)
237247

@@ -269,7 +279,7 @@ func (c *ApiClient) SendMultipartRequest(method, endpoint string, headers []stri
269279
}
270280

271281
// SendMultipartRequestWithBuffer sends an HTTP request with multipart form data using a buffer for file data
272-
func (c *ApiClient) SendMultipartRequestWithBuffer(method, endpoint string, headers []string, formFields map[string]string, fileField, fileName string, fileData []byte, authType string, username string, verbose bool) (json.RawMessage, *xurlErrors.Error) {
282+
func (c *ApiClient) SendMultipartRequestWithBuffer(method, endpoint string, headers []string, formFields map[string]string, fileField, fileName string, fileData []byte, authType string, username string, verbose bool) (json.RawMessage, error) {
273283
body := &bytes.Buffer{}
274284
writer := multipart.NewWriter(body)
275285

@@ -301,7 +311,7 @@ func (c *ApiClient) SendMultipartRequestWithBuffer(method, endpoint string, head
301311
}
302312

303313
// StreamRequest sends an HTTP request and streams the response
304-
func (c *ApiClient) StreamRequest(method, endpoint string, headers []string, data string, authType string, username string, verbose bool) *xurlErrors.Error {
314+
func (c *ApiClient) StreamRequest(method, endpoint string, headers []string, data string, authType string, username string, verbose bool) error {
305315
var body io.Reader
306316
contentType := ""
307317

api/client_test.go

Lines changed: 36 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ import (
1515
"xurl/config"
1616
xurlErrors "xurl/errors"
1717
"xurl/store"
18+
19+
"github.com/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
1821
)
1922

2023
// Helper function to create a temporary token store for testing
2124
func createTempTokenStore(t *testing.T) (*store.TokenStore, string) {
22-
// Create a temporary directory for testing
2325
tempDir, err := os.MkdirTemp("", "xurl_test")
2426
if err != nil {
2527
t.Fatalf("Failed to create temp directory: %v", err)
2628
}
2729

28-
// Create a token store with a file in the temp directory
2930
tempFile := filepath.Join(tempDir, "tokens.json")
3031
tokenStore := &store.TokenStore{
3132
OAuth2Tokens: make(map[string]store.Token),
@@ -50,7 +51,6 @@ func createMockAuth(t *testing.T) (*auth.Auth, string) {
5051
mockAuth := auth.NewAuth(cfg)
5152
tokenStore, tempDir := createTempTokenStore(t)
5253

53-
// Add a test bearer token
5454
err := tokenStore.SaveBearerToken("test-bearer-token")
5555
if err != nil {
5656
t.Fatalf("Failed to save bearer token: %v", err)
@@ -69,17 +69,9 @@ func TestNewApiClient(t *testing.T) {
6969

7070
client := NewApiClient(cfg, auth)
7171

72-
if client.url != cfg.APIBaseURL {
73-
t.Errorf("Expected URL to be %s, got %s", cfg.APIBaseURL, client.url)
74-
}
75-
76-
if client.auth != auth {
77-
t.Errorf("Expected auth to be set correctly")
78-
}
79-
80-
if client.client == nil {
81-
t.Errorf("HTTP client should not be nil")
82-
}
72+
assert.Equal(t, cfg.APIBaseURL, client.url, "URL should match config")
73+
assert.Equal(t, auth, client.auth, "Auth should be set correctly")
74+
assert.NotNil(t, client.client, "HTTP client should not be nil")
8375
}
8476

8577
func TestBuildRequest(t *testing.T) {
@@ -159,43 +151,29 @@ func TestBuildRequest(t *testing.T) {
159151

160152
req, err := client.BuildRequest(tt.method, tt.endpoint, tt.headers, body, contentType, tt.authType, tt.username)
161153

162-
if (err != nil) != tt.wantErr {
163-
t.Errorf("BuildRequest() error = %v, wantErr %v", err, tt.wantErr)
154+
if tt.wantErr {
155+
assert.Error(t, err)
164156
return
165157
}
166158

167-
if err != nil {
168-
return
169-
}
170-
171-
if req.Method != tt.wantMethod {
172-
t.Errorf("BuildRequest() method = %v, want %v", req.Method, tt.wantMethod)
173-
}
174-
175-
if req.URL.String() != tt.wantURL {
176-
t.Errorf("BuildRequest() URL = %v, want %v", req.URL.String(), tt.wantURL)
177-
}
159+
require.NoError(t, err)
160+
assert.Equal(t, tt.wantMethod, req.Method)
161+
assert.Equal(t, tt.wantURL, req.URL.String())
178162

179163
for _, header := range tt.headers {
180164
parts := strings.Split(header, ": ")
181-
if len(parts) != 2 {
182-
t.Errorf("Invalid header format: %s", header)
183-
continue
184-
}
165+
require.Len(t, parts, 2, "Invalid header format: %s", header)
185166

186167
key := strings.TrimSpace(parts[0])
187168
value := strings.TrimSpace(parts[1])
188169

189-
if req.Header.Get(key) != value {
190-
t.Errorf("BuildRequest() header %s = %s, want %s", key, req.Header.Get(key), value)
191-
}
192-
}
170+
assert.Equal(t, value, req.Header.Get(key))
171+
}
172+
173+
assert.Equal(t, "xurl/dev", req.Header.Get("User-Agent"))
193174

194175
if tt.method == "POST" && tt.data != "" {
195-
contentType := req.Header.Get("Content-Type")
196-
if contentType != "application/json" {
197-
t.Errorf("Expected Content-Type header to be application/json, got %s", contentType)
198-
}
176+
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
199177
}
200178
})
201179
}
@@ -228,7 +206,6 @@ func TestSendRequest(t *testing.T) {
228206
}))
229207
defer server.Close()
230208

231-
// Setup client
232209
cfg := &config.Config{
233210
APIBaseURL: server.URL,
234211
}
@@ -240,70 +217,40 @@ func TestSendRequest(t *testing.T) {
240217
t.Run("Get user profile", func(t *testing.T) {
241218
resp, err := client.SendRequest("GET", "/2/users/me", []string{"Authorization: Bearer test-token"}, "", "", "", false)
242219

243-
if err != nil {
244-
t.Errorf("SendRequest() error = %v", err)
245-
return
246-
}
220+
require.NoError(t, err)
247221

248222
var result map[string]interface{}
249-
if e := json.Unmarshal(resp, &result); e != nil {
250-
t.Errorf("Failed to parse response: %v", e)
251-
return
252-
}
223+
err = json.Unmarshal(resp, &result)
224+
require.NoError(t, err, "Failed to parse response")
253225

254226
data, ok := result["data"].(map[string]interface{})
255-
if !ok {
256-
t.Errorf("Expected data object in response")
257-
return
258-
}
227+
require.True(t, ok, "Expected data object in response")
259228

260-
if username, ok := data["username"]; !ok || username != "testuser" {
261-
t.Errorf("Expected username 'testuser', got %v", username)
262-
}
229+
assert.Equal(t, "testuser", data["username"], "Username should match")
263230
})
264231

265232
// Test successful POST request
266233
t.Run("Post tweet", func(t *testing.T) {
267234
resp, err := client.SendRequest("POST", "/2/tweets", []string{"Authorization: Bearer test-token"}, `{"text":"Hello world!"}`, "", "", false)
268235

269-
if err != nil {
270-
t.Errorf("SendRequest() error = %v", err)
271-
return
272-
}
236+
require.NoError(t, err)
273237

274238
var result map[string]interface{}
275-
if e := json.Unmarshal(resp, &result); e != nil {
276-
t.Errorf("Failed to parse response: %v", e)
277-
return
278-
}
239+
err = json.Unmarshal(resp, &result)
240+
require.NoError(t, err, "Failed to parse response")
279241

280242
data, ok := result["data"].(map[string]interface{})
281-
if !ok {
282-
t.Errorf("Expected data object in response")
283-
return
284-
}
243+
require.True(t, ok, "Expected data object in response")
285244

286-
if text, ok := data["text"]; !ok || text != "Hello world!" {
287-
t.Errorf("Expected text 'Hello world!', got %v", text)
288-
}
245+
assert.Equal(t, "Hello world!", data["text"], "Tweet text should match")
289246
})
290247

291-
// Test error response
292248
t.Run("Error response", func(t *testing.T) {
293249
resp, err := client.SendRequest("GET", "/2/tweets/search/recent", []string{"Authorization: Bearer test-token"}, "", "", "", false)
294250

295-
if err == nil {
296-
t.Errorf("SendRequest() expected error, got nil")
297-
return
298-
}
299-
300-
if resp != nil {
301-
t.Errorf("SendRequest() expected nil response, got %v", resp)
302-
}
303-
304-
if !xurlErrors.IsAPIError(err) {
305-
t.Errorf("Expected API error, got %v", err)
306-
}
251+
assert.Error(t, err, "Expected an error")
252+
assert.Nil(t, resp, "Response should be nil")
253+
assert.True(t, xurlErrors.IsAPIError(err), "Expected API error")
307254
})
308255
}
309256

@@ -317,13 +264,8 @@ func TestGetAuthHeader(t *testing.T) {
317264

318265
_, err := client.GetAuthHeader("GET", "https://api.x.com/2/users/me", "", "")
319266

320-
if err == nil {
321-
t.Errorf("GetAuthHeader() expected error, got nil")
322-
}
323-
324-
if !xurlErrors.IsAuthError(err) {
325-
t.Errorf("Expected auth error, got %v", err)
326-
}
267+
assert.Error(t, err, "Expected an error")
268+
assert.True(t, xurlErrors.IsAuthError(err), "Expected auth error")
327269
})
328270

329271
t.Run("Invalid auth type", func(t *testing.T) {
@@ -333,13 +275,8 @@ func TestGetAuthHeader(t *testing.T) {
333275

334276
_, err := client.GetAuthHeader("GET", "https://api.x.com/2/users/me", "invalid", "")
335277

336-
if err == nil {
337-
t.Errorf("GetAuthHeader() expected error, got nil")
338-
}
339-
340-
if !xurlErrors.IsAuthError(err) {
341-
t.Errorf("Expected auth error, got %v", err)
342-
}
278+
assert.Error(t, err, "Expected an error")
279+
assert.True(t, xurlErrors.IsAuthError(err), "Expected auth error")
343280
})
344281
}
345282

@@ -377,14 +314,7 @@ func TestStreamRequest(t *testing.T) {
377314
t.Run("Stream error response", func(t *testing.T) {
378315
err := client.StreamRequest("GET", "/2/tweets/search/stream/error", []string{"Authorization: Bearer test-token"}, "", "", "", false)
379316

380-
if err == nil {
381-
t.Errorf("StreamRequest() expected error, got nil")
382-
return
383-
}
384-
385-
// Check if it's an API error
386-
if !xurlErrors.IsAPIError(err) {
387-
t.Errorf("Expected API error, got %v", err)
388-
}
317+
assert.Error(t, err, "Expected an error")
318+
assert.True(t, xurlErrors.IsAPIError(err), "Expected API error")
389319
})
390320
}

api/endpoints_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package api
22

33
import (
44
"testing"
5+
6+
"github.com/stretchr/testify/assert"
57
)
68

79
func TestIsStreamingEndpoint(t *testing.T) {
@@ -41,9 +43,7 @@ func TestIsStreamingEndpoint(t *testing.T) {
4143
for _, tc := range testCases {
4244
t.Run(tc.endpoint, func(t *testing.T) {
4345
result := IsStreamingEndpoint(tc.endpoint)
44-
if result != tc.expected {
45-
t.Errorf("IsStreamingEndpoint(%q) = %v, expected %v", tc.endpoint, result, tc.expected)
46-
}
46+
assert.Equal(t, tc.expected, result, "IsStreamingEndpoint(%q) should return %v", tc.endpoint, tc.expected)
4747
})
4848
}
4949
}

api/execute.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ package api
33
import (
44
"encoding/json"
55
"fmt"
6-
"xurl/errors"
76
"xurl/utils"
87
)
98

109
// ExecuteRequest handles the execution of a regular API request
11-
func ExecuteRequest(method, url string, headers []string, data, authType, username string, verbose bool, client *ApiClient) error {
10+
func ExecuteRequest(method, url string, headers []string, data, authType, username string, verbose bool, client Client) error {
1211
response, clientErr := client.SendRequest(method, url, headers, data, authType, username, verbose)
1312
if clientErr != nil {
1413
return handleRequestError(clientErr)
@@ -18,7 +17,7 @@ func ExecuteRequest(method, url string, headers []string, data, authType, userna
1817
}
1918

2019
// ExecuteStreamRequest handles the execution of a streaming API request
21-
func ExecuteStreamRequest(method, url string, headers []string, data, authType, username string, verbose bool, client *ApiClient) error {
20+
func ExecuteStreamRequest(method, url string, headers []string, data, authType, username string, verbose bool, client Client) error {
2221
clientErr := client.StreamRequest(method, url, headers, data, authType, username, verbose)
2322
if clientErr != nil {
2423
return handleRequestError(clientErr)
@@ -28,17 +27,17 @@ func ExecuteStreamRequest(method, url string, headers []string, data, authType,
2827
}
2928

3029
// handleRequestError processes API client errors in a consistent way
31-
func handleRequestError(clientErr *errors.Error) error {
30+
func handleRequestError(clientErr error) error {
3231
var rawJSON json.RawMessage
33-
json.Unmarshal([]byte(clientErr.Message), &rawJSON)
32+
json.Unmarshal([]byte(clientErr.Error()), &rawJSON)
3433
utils.FormatAndPrintResponse(rawJSON)
3534
return fmt.Errorf("request failed")
3635
}
3736

3837
// formatAndPrintResponse formats and prints API responses
3938

4039
// HandleRequest determines the type of request and executes it accordingly
41-
func HandleRequest(method, url string, headers []string, data, authType, username string, verbose, forceStream bool, mediaFile string, client *ApiClient) error {
40+
func HandleRequest(method, url string, headers []string, data, authType, username string, verbose, forceStream bool, mediaFile string, client Client) error {
4241
if IsMediaAppendRequest(url, mediaFile) {
4342
response, err := HandleMediaAppendRequest(url, mediaFile, method, headers, data, authType, username, verbose, client)
4443
if err != nil {

0 commit comments

Comments
 (0)