Skip to content

Commit 0974a81

Browse files
committed
Refactor API client to utilize structured request options and enhance code clarity across the application.
1 parent 28ca3ac commit 0974a81

21 files changed

Lines changed: 734 additions & 613 deletions

api/client.go

Lines changed: 110 additions & 124 deletions
Large diffs are not rendered by default.

api/client_test.go

Lines changed: 90 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ func createTempTokenStore(t *testing.T) (*store.TokenStore, string) {
2626
if err != nil {
2727
t.Fatalf("Failed to create temp directory: %v", err)
2828
}
29-
29+
3030
tempFile := filepath.Join(tempDir, "tokens.json")
3131
tokenStore := &store.TokenStore{
3232
OAuth2Tokens: make(map[string]store.Token),
3333
FilePath: tempFile,
3434
}
35-
35+
3636
return tokenStore, tempDir
3737
}
3838

@@ -47,15 +47,15 @@ func createMockAuth(t *testing.T) (*auth.Auth, string) {
4747
APIBaseURL: "https://api.x.com",
4848
InfoURL: "https://api.x.com/2/users/me",
4949
}
50-
50+
5151
mockAuth := auth.NewAuth(cfg)
5252
tokenStore, tempDir := createTempTokenStore(t)
53-
53+
5454
err := tokenStore.SaveBearerToken("test-bearer-token")
5555
if err != nil {
5656
t.Fatalf("Failed to save bearer token: %v", err)
5757
}
58-
58+
5959
mockAuth.WithTokenStore(tokenStore)
6060
return mockAuth, tempDir
6161
}
@@ -66,9 +66,9 @@ func TestNewApiClient(t *testing.T) {
6666
}
6767
auth, tempDir := createMockAuth(t)
6868
defer os.RemoveAll(tempDir)
69-
69+
7070
client := NewApiClient(cfg, auth)
71-
71+
7272
assert.Equal(t, cfg.APIBaseURL, client.url, "URL should match config")
7373
assert.Equal(t, auth, client.auth, "Auth should be set correctly")
7474
assert.NotNil(t, client.client, "HTTP client should not be nil")
@@ -81,9 +81,9 @@ func TestBuildRequest(t *testing.T) {
8181
}
8282
authMock, tempDir := createMockAuth(t)
8383
defer os.RemoveAll(tempDir)
84-
84+
8585
client := NewApiClient(cfg, authMock)
86-
86+
8787
tests := []struct {
8888
name string
8989
method string
@@ -133,45 +133,45 @@ func TestBuildRequest(t *testing.T) {
133133
wantErr: false,
134134
},
135135
}
136-
136+
137137
for _, tt := range tests {
138138
t.Run(tt.name, func(t *testing.T) {
139139
var body io.Reader
140140
contentType := ""
141141
if tt.data != "" && (strings.ToUpper(tt.method) == "POST" || strings.ToUpper(tt.method) == "PUT" || strings.ToUpper(tt.method) == "PATCH") {
142142
body = bytes.NewBufferString(tt.data)
143-
143+
144144
var js json.RawMessage
145145
if json.Unmarshal([]byte(tt.data), &js) == nil {
146146
contentType = "application/json"
147147
} else {
148148
contentType = "application/x-www-form-urlencoded"
149149
}
150150
}
151-
151+
152152
req, err := client.BuildRequest(tt.method, tt.endpoint, tt.headers, body, contentType, tt.authType, tt.username)
153-
153+
154154
if tt.wantErr {
155155
assert.Error(t, err)
156156
return
157157
}
158-
158+
159159
require.NoError(t, err)
160160
assert.Equal(t, tt.wantMethod, req.Method)
161161
assert.Equal(t, tt.wantURL, req.URL.String())
162-
162+
163163
for _, header := range tt.headers {
164164
parts := strings.Split(header, ": ")
165165
require.Len(t, parts, 2, "Invalid header format: %s", header)
166-
166+
167167
key := strings.TrimSpace(parts[0])
168168
value := strings.TrimSpace(parts[1])
169-
169+
170170
assert.Equal(t, value, req.Header.Get(key))
171171
}
172172

173173
assert.Equal(t, "xurl/dev", req.Header.Get("User-Agent"))
174-
174+
175175
if tt.method == "POST" && tt.data != "" {
176176
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
177177
}
@@ -187,67 +187,97 @@ func TestSendRequest(t *testing.T) {
187187
w.Write([]byte(`{"data":{"id":"12345","name":"Test User","username":"testuser"}}`))
188188
return
189189
}
190-
190+
191191
if r.URL.Path == "/2/tweets" && r.Method == "POST" {
192192
w.Header().Set("Content-Type", "application/json")
193193
w.WriteHeader(http.StatusCreated)
194194
w.Write([]byte(`{"data":{"id":"67890","text":"Hello world!"}}`))
195195
return
196196
}
197-
197+
198198
if r.URL.Path == "/2/tweets/search/recent" {
199199
w.Header().Set("Content-Type", "application/json")
200200
w.WriteHeader(http.StatusBadRequest)
201201
w.Write([]byte(`{"errors":[{"message":"Invalid query","code":400}]}`))
202202
return
203203
}
204-
204+
205205
w.WriteHeader(http.StatusNotFound)
206206
}))
207207
defer server.Close()
208-
208+
209209
cfg := &config.Config{
210210
APIBaseURL: server.URL,
211211
}
212212
authMock, tempDir := createMockAuth(t)
213213
defer os.RemoveAll(tempDir)
214214
client := NewApiClient(cfg, authMock)
215-
215+
216216
// Test successful GET request
217217
t.Run("Get user profile", func(t *testing.T) {
218-
resp, err := client.SendRequest("GET", "/2/users/me", []string{"Authorization: Bearer test-token"}, "", "", "", false)
219-
218+
options := RequestOptions{
219+
Method: "GET",
220+
Endpoint: "/2/users/me",
221+
Headers: []string{"Authorization: Bearer test-token"},
222+
Data: "",
223+
AuthType: "",
224+
Username: "",
225+
Verbose: false,
226+
}
227+
228+
resp, err := client.SendRequest(options)
229+
220230
require.NoError(t, err)
221-
231+
222232
var result map[string]interface{}
223233
err = json.Unmarshal(resp, &result)
224234
require.NoError(t, err, "Failed to parse response")
225-
235+
226236
data, ok := result["data"].(map[string]interface{})
227237
require.True(t, ok, "Expected data object in response")
228-
238+
229239
assert.Equal(t, "testuser", data["username"], "Username should match")
230240
})
231-
241+
232242
// Test successful POST request
233243
t.Run("Post tweet", func(t *testing.T) {
234-
resp, err := client.SendRequest("POST", "/2/tweets", []string{"Authorization: Bearer test-token"}, `{"text":"Hello world!"}`, "", "", false)
235-
244+
options := RequestOptions{
245+
Method: "POST",
246+
Endpoint: "/2/tweets",
247+
Headers: []string{"Authorization: Bearer test-token"},
248+
Data: `{"text":"Hello world!"}`,
249+
AuthType: "",
250+
Username: "",
251+
Verbose: false,
252+
}
253+
254+
resp, err := client.SendRequest(options)
255+
236256
require.NoError(t, err)
237-
257+
238258
var result map[string]interface{}
239259
err = json.Unmarshal(resp, &result)
240260
require.NoError(t, err, "Failed to parse response")
241-
261+
242262
data, ok := result["data"].(map[string]interface{})
243263
require.True(t, ok, "Expected data object in response")
244-
264+
245265
assert.Equal(t, "Hello world!", data["text"], "Tweet text should match")
246266
})
247-
267+
248268
t.Run("Error response", func(t *testing.T) {
249-
resp, err := client.SendRequest("GET", "/2/tweets/search/recent", []string{"Authorization: Bearer test-token"}, "", "", "", false)
250-
269+
options := RequestOptions{
270+
Method: "GET",
271+
Endpoint: "/2/tweets/search/recent",
272+
Headers: []string{"Authorization: Bearer test-token"},
273+
Data: "",
274+
AuthType: "",
275+
Username: "",
276+
Verbose: false,
277+
}
278+
279+
resp, err := client.SendRequest(options)
280+
251281
assert.Error(t, err, "Expected an error")
252282
assert.Nil(t, resp, "Response should be nil")
253283
assert.True(t, xurlErrors.IsAPIError(err), "Expected API error")
@@ -258,23 +288,23 @@ func TestGetAuthHeader(t *testing.T) {
258288
cfg := &config.Config{
259289
APIBaseURL: "https://api.x.com",
260290
}
261-
291+
262292
t.Run("No auth set", func(t *testing.T) {
263293
client := NewApiClient(cfg, nil)
264-
294+
265295
_, err := client.GetAuthHeader("GET", "https://api.x.com/2/users/me", "", "")
266-
296+
267297
assert.Error(t, err, "Expected an error")
268298
assert.True(t, xurlErrors.IsAuthError(err), "Expected auth error")
269299
})
270-
300+
271301
t.Run("Invalid auth type", func(t *testing.T) {
272302
authMock, tempDir := createMockAuth(t)
273303
defer os.RemoveAll(tempDir)
274304
client := NewApiClient(cfg, authMock)
275-
305+
276306
_, err := client.GetAuthHeader("GET", "https://api.x.com/2/users/me", "invalid", "")
277-
307+
278308
assert.Error(t, err, "Expected an error")
279309
assert.True(t, xurlErrors.IsAuthError(err), "Expected auth error")
280310
})
@@ -283,7 +313,7 @@ func TestGetAuthHeader(t *testing.T) {
283313
func TestStreamRequest(t *testing.T) {
284314
// This is a basic test for the StreamRequest method
285315
// A more comprehensive test would require mocking the streaming response
286-
316+
287317
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
288318
if r.URL.Path == "/2/tweets/search/stream" {
289319
w.Header().Set("Content-Type", "application/json")
@@ -292,28 +322,38 @@ func TestStreamRequest(t *testing.T) {
292322
// but for this simple test, we'll just close the connection
293323
return
294324
}
295-
325+
296326
if r.URL.Path == "/2/tweets/search/stream/error" {
297327
w.Header().Set("Content-Type", "application/json")
298328
w.WriteHeader(http.StatusBadRequest)
299329
w.Write([]byte(`{"errors":[{"message":"Invalid rule","code":400}]}`))
300330
return
301331
}
302-
332+
303333
w.WriteHeader(http.StatusNotFound)
304334
}))
305335
defer server.Close()
306-
336+
307337
cfg := &config.Config{
308338
APIBaseURL: server.URL,
309339
}
310340
authMock, tempDir := createMockAuth(t)
311341
defer os.RemoveAll(tempDir)
312342
client := NewApiClient(cfg, authMock)
313-
343+
314344
t.Run("Stream error response", func(t *testing.T) {
315-
err := client.StreamRequest("GET", "/2/tweets/search/stream/error", []string{"Authorization: Bearer test-token"}, "", "", "", false)
316-
345+
options := RequestOptions{
346+
Method: "GET",
347+
Endpoint: "/2/tweets/search/stream/error",
348+
Headers: []string{"Authorization: Bearer test-token"},
349+
Data: "",
350+
AuthType: "",
351+
Username: "",
352+
Verbose: false,
353+
}
354+
355+
err := client.StreamRequest(options)
356+
317357
assert.Error(t, err, "Expected an error")
318358
assert.True(t, xurlErrors.IsAPIError(err), "Expected API error")
319359
})

api/endpoints.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66

77
// StreamingEndpoints is a map of endpoint prefixes that should be streamed
88
var StreamingEndpoints = map[string]bool{
9-
"/2/tweets/search/stream": true,
10-
"/2/tweets/sample/stream": true,
11-
"/2/tweets/sample10/stream": true,
12-
"/2/tweets/firehose/stream": true,
9+
"/2/tweets/search/stream": true,
10+
"/2/tweets/sample/stream": true,
11+
"/2/tweets/sample10/stream": true,
12+
"/2/tweets/firehose/stream": true,
1313
"/2/tweets/firehose/stream/lang/en": true,
1414
"/2/tweets/firehose/stream/lang/ja": true,
1515
"/2/tweets/firehose/stream/lang/ko": true,
@@ -25,18 +25,18 @@ func IsStreamingEndpoint(endpoint string) bool {
2525
path = "/" + parsedURL[3]
2626
}
2727
}
28-
28+
2929
normalizedEndpoint := strings.TrimSuffix(path, "/")
30-
30+
3131
if StreamingEndpoints[normalizedEndpoint] {
3232
return true
3333
}
34-
34+
3535
for streamingEndpoint := range StreamingEndpoints {
3636
if strings.HasPrefix(normalizedEndpoint, streamingEndpoint) {
3737
return true
3838
}
3939
}
40-
40+
4141
return false
42-
}
42+
}

api/endpoints_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,30 @@ func TestIsStreamingEndpoint(t *testing.T) {
2020
{"/2/tweets/firehose/stream/lang/ja", true},
2121
{"/2/tweets/firehose/stream/lang/ko", true},
2222
{"/2/tweets/firehose/stream/lang/pt", true},
23-
23+
2424
// Test with trailing slash
2525
{"/2/tweets/search/stream/", true},
26-
26+
2727
// Test with query parameters
2828
{"/2/tweets/search/stream?query=test", true},
29-
29+
3030
// Test with full URL
3131
{"https://api.x.com/2/tweets/search/stream", true},
3232
{"http://api.x.com/2/tweets/search/stream", true},
3333
{"https://api.x.com/2/tweets/search/stream?query=test", true},
34-
34+
3535
// Test non-streaming endpoints
3636
{"/2/tweets/search/recent", false},
3737
{"/2/users/me", false},
3838
{"https://api.x.com/2/users/me", false},
3939
{"/not/a/streaming/endpoint", false},
4040
{"", false},
4141
}
42-
42+
4343
for _, tc := range testCases {
4444
t.Run(tc.endpoint, func(t *testing.T) {
4545
result := IsStreamingEndpoint(tc.endpoint)
4646
assert.Equal(t, tc.expected, result, "IsStreamingEndpoint(%q) should return %v", tc.endpoint, tc.expected)
4747
})
4848
}
49-
}
49+
}

0 commit comments

Comments
 (0)