Skip to content

Commit 51e76d5

Browse files
authored
fix: library improvements - bug fixes, tests, and coverage badge (#19)
2 parents e5ee5e7 + 0819e67 commit 51e76d5

19 files changed

+1466
-26
lines changed

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,26 @@ jobs:
4949
gofmt -s -l . | grep -v vendor
5050
exit 1
5151
fi
52+
53+
- name: Run tests with coverage
54+
run: go test -short -race -coverprofile=coverage.out ./...
55+
56+
- name: Extract coverage percentage
57+
if: github.ref == 'refs/heads/main' && matrix.go-version == '1.24'
58+
id: coverage
59+
run: |
60+
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
61+
echo "percentage=$COVERAGE" >> "$GITHUB_OUTPUT"
62+
63+
- name: Update coverage badge
64+
if: github.ref == 'refs/heads/main' && matrix.go-version == '1.24'
65+
uses: schneegans/dynamic-badges-action@v1.7.0
66+
with:
67+
auth: ${{ secrets.GIST_TOKEN }}
68+
gistID: 2c608589294aed9aa900256daeec0fd4
69+
filename: coverage.json
70+
label: coverage
71+
message: ${{ steps.coverage.outputs.percentage }}%
72+
valColorRange: ${{ steps.coverage.outputs.percentage }}
73+
minColorRange: 40
74+
maxColorRange: 90

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
[![Go Reference](https://pkg.go.dev/badge/github.com/tirthpatell/threads-go.svg)](https://pkg.go.dev/github.com/tirthpatell/threads-go)
44
[![Go Report Card](https://goreportcard.com/badge/github.com/tirthpatell/threads-go)](https://goreportcard.com/report/github.com/tirthpatell/threads-go)
55
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6+
[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tirthpatell/2c608589294aed9aa900256daeec0fd4/raw/coverage.json)](https://github.com/tirthpatell/threads-go/actions)
67

78
Production-ready Go client for the Threads API with complete endpoint coverage, OAuth 2.0 authentication, rate limiting, and comprehensive error handling.
89

auth_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package threads
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"strings"
8+
"testing"
9+
"time"
10+
)
11+
12+
func TestExchangeCodeForToken_Success(t *testing.T) {
13+
handler := func(w http.ResponseWriter, r *http.Request) {
14+
if r.Method != "POST" {
15+
http.NotFound(w, r)
16+
return
17+
}
18+
w.Header().Set("Content-Type", "application/json")
19+
w.WriteHeader(200)
20+
_, _ = w.Write([]byte(`{
21+
"access_token": "new_token_123",
22+
"token_type": "bearer",
23+
"expires_in": 3600,
24+
"user_id": 99999
25+
}`))
26+
}
27+
28+
server := httptest.NewServer(http.HandlerFunc(handler))
29+
t.Cleanup(server.Close)
30+
31+
config := &Config{
32+
ClientID: "test-id",
33+
ClientSecret: "test-secret",
34+
RedirectURI: "https://example.com/callback",
35+
}
36+
config.SetDefaults()
37+
config.BaseURL = server.URL
38+
39+
client, err := NewClient(config)
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
44+
err = client.ExchangeCodeForToken(context.Background(), "auth_code_123")
45+
if err != nil {
46+
t.Fatalf("unexpected error: %v", err)
47+
}
48+
if !client.IsAuthenticated() {
49+
t.Error("expected client to be authenticated")
50+
}
51+
tokenInfo := client.GetTokenInfo()
52+
if tokenInfo.AccessToken != "new_token_123" {
53+
t.Errorf("expected new_token_123, got %s", tokenInfo.AccessToken)
54+
}
55+
if tokenInfo.UserID != "99999" {
56+
t.Errorf("expected user ID 99999, got %s", tokenInfo.UserID)
57+
}
58+
}
59+
60+
func TestExchangeCodeForToken_EmptyCode(t *testing.T) {
61+
config := &Config{
62+
ClientID: "test-id",
63+
ClientSecret: "test-secret",
64+
RedirectURI: "https://example.com/callback",
65+
}
66+
config.SetDefaults()
67+
client, _ := NewClient(config)
68+
69+
err := client.ExchangeCodeForToken(context.Background(), "")
70+
if err == nil {
71+
t.Fatal("expected error for empty code")
72+
}
73+
}
74+
75+
func TestGetLongLivedToken_Success(t *testing.T) {
76+
client := testClient(t, jsonHandler(200, `{
77+
"access_token": "long_lived_token",
78+
"token_type": "bearer",
79+
"expires_in": 5184000
80+
}`))
81+
82+
err := client.GetLongLivedToken(context.Background())
83+
if err != nil {
84+
t.Fatalf("unexpected error: %v", err)
85+
}
86+
tokenInfo := client.GetTokenInfo()
87+
if tokenInfo.AccessToken != "long_lived_token" {
88+
t.Errorf("expected long_lived_token, got %s", tokenInfo.AccessToken)
89+
}
90+
}
91+
92+
func TestRefreshToken_Success(t *testing.T) {
93+
client := testClient(t, jsonHandler(200, `{
94+
"access_token": "refreshed_token",
95+
"token_type": "bearer",
96+
"expires_in": 5184000
97+
}`))
98+
99+
err := client.RefreshToken(context.Background())
100+
if err != nil {
101+
t.Fatalf("unexpected error: %v", err)
102+
}
103+
tokenInfo := client.GetTokenInfo()
104+
if tokenInfo.AccessToken != "refreshed_token" {
105+
t.Errorf("expected refreshed_token, got %s", tokenInfo.AccessToken)
106+
}
107+
}
108+
109+
func TestRefreshToken_NoToken(t *testing.T) {
110+
config := &Config{
111+
ClientID: "test-id",
112+
ClientSecret: "test-secret",
113+
RedirectURI: "https://example.com/callback",
114+
}
115+
config.SetDefaults()
116+
client, _ := NewClient(config)
117+
118+
err := client.RefreshToken(context.Background())
119+
if err == nil {
120+
t.Fatal("expected error when no token")
121+
}
122+
}
123+
124+
func TestDebugToken_Success(t *testing.T) {
125+
client := testClient(t, jsonHandler(200, `{
126+
"data": {
127+
"type": "USER",
128+
"application": "Test App",
129+
"is_valid": true,
130+
"expires_at": 1735689600,
131+
"issued_at": 1735603200,
132+
"user_id": "12345",
133+
"scopes": ["threads_basic"]
134+
}
135+
}`))
136+
137+
resp, err := client.DebugToken(context.Background(), "test-token")
138+
if err != nil {
139+
t.Fatalf("unexpected error: %v", err)
140+
}
141+
if !resp.Data.IsValid {
142+
t.Error("expected valid token")
143+
}
144+
if resp.Data.UserID != "12345" {
145+
t.Errorf("expected user ID 12345, got %s", resp.Data.UserID)
146+
}
147+
}
148+
149+
func TestGetAuthURL_ContainsRequiredParams(t *testing.T) {
150+
config := &Config{
151+
ClientID: "my-app-id",
152+
ClientSecret: "secret",
153+
RedirectURI: "https://example.com/callback",
154+
}
155+
config.SetDefaults()
156+
client, _ := NewClient(config)
157+
158+
authURL := client.GetAuthURL([]string{"threads_basic"})
159+
if authURL == "" {
160+
t.Fatal("expected non-empty auth URL")
161+
}
162+
for _, param := range []string{"client_id=my-app-id", "response_type=code", "scope=threads_basic"} {
163+
if !strings.Contains(authURL, param) {
164+
t.Errorf("expected auth URL to contain %q, got %s", param, authURL)
165+
}
166+
}
167+
}
168+
169+
func TestTokenExpiration(t *testing.T) {
170+
config := &Config{
171+
ClientID: "test-id",
172+
ClientSecret: "test-secret",
173+
RedirectURI: "https://example.com/callback",
174+
}
175+
config.SetDefaults()
176+
client, _ := NewClient(config)
177+
178+
_ = client.SetTokenInfo(&TokenInfo{
179+
AccessToken: "expired",
180+
TokenType: "Bearer",
181+
ExpiresAt: time.Now().Add(-time.Hour),
182+
UserID: "12345",
183+
CreatedAt: time.Now().Add(-2 * time.Hour),
184+
})
185+
186+
if !client.IsTokenExpired() {
187+
t.Error("expected token to be expired")
188+
}
189+
if !client.IsTokenExpiringSoon(time.Hour) {
190+
t.Error("expected token to be expiring soon")
191+
}
192+
}

client_test.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -682,13 +682,6 @@ func TestCreateErrorFromResponseParsesIsTransient(t *testing.T) {
682682
}
683683
}
684684

685-
type noopLogger struct{}
686-
687-
func (n *noopLogger) Debug(msg string, fields ...any) {}
688-
func (n *noopLogger) Info(msg string, fields ...any) {}
689-
func (n *noopLogger) Warn(msg string, fields ...any) {}
690-
func (n *noopLogger) Error(msg string, fields ...any) {}
691-
692685
func TestIsRetryableErrorWithTransientAPIError(t *testing.T) {
693686
h := &HTTPClient{
694687
logger: &noopLogger{},

client_utils.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import (
88

99
// getUserID extracts user ID from token info
1010
func (c *Client) getUserID() string {
11+
c.mu.RLock()
12+
defer c.mu.RUnlock()
1113
if c.tokenInfo != nil && c.tokenInfo.UserID != "" {
1214
return c.tokenInfo.UserID
1315
}
14-
15-
// If user ID is not in token info, we might need to call /me endpoint
16-
// For now, return empty string to trigger an error
1716
return ""
1817
}
1918

http_client.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,23 @@ func NewHTTPClient(config *Config, rateLimiter *RateLimiter) *HTTPClient {
5858
Timeout: config.HTTPTimeout,
5959
}
6060

61+
baseURL := config.BaseURL
62+
if baseURL == "" {
63+
baseURL = "https://graph.threads.net"
64+
}
65+
66+
userAgent := config.UserAgent
67+
if userAgent == "" {
68+
userAgent = DefaultUserAgent
69+
}
70+
6171
return &HTTPClient{
6272
client: httpClient,
6373
logger: config.Logger,
6474
retryConfig: config.RetryConfig,
6575
rateLimiter: rateLimiter,
66-
baseURL: "https://graph.threads.net",
67-
userAgent: DefaultUserAgent,
76+
baseURL: baseURL,
77+
userAgent: userAgent,
6878
}
6979
}
7080

0 commit comments

Comments
 (0)