A community based topic aggregation platform built on atproto
1package imageproxy
2
3import (
4 "context"
5 "errors"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9 "time"
10)
11
12func TestPDSFetcher_Fetch_Success(t *testing.T) {
13 // Setup test server that returns blob data
14 expectedData := []byte("test image data")
15 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16 // Verify the request path and query parameters
17 if r.URL.Path != "/xrpc/com.atproto.sync.getBlob" {
18 t.Errorf("unexpected path: %s", r.URL.Path)
19 }
20 if r.URL.Query().Get("did") != "did:plc:test123" {
21 t.Errorf("unexpected did: %s", r.URL.Query().Get("did"))
22 }
23 if r.URL.Query().Get("cid") != "bafyreicid123" {
24 t.Errorf("unexpected cid: %s", r.URL.Query().Get("cid"))
25 }
26 w.WriteHeader(http.StatusOK)
27 w.Write(expectedData)
28 }))
29 defer server.Close()
30
31 fetcher := NewPDSFetcher(5 * time.Second, 10)
32 ctx := context.Background()
33
34 data, err := fetcher.Fetch(ctx, server.URL, "did:plc:test123", "bafyreicid123")
35 if err != nil {
36 t.Fatalf("expected no error, got: %v", err)
37 }
38 if string(data) != string(expectedData) {
39 t.Errorf("expected data %q, got %q", expectedData, data)
40 }
41}
42
43func TestPDSFetcher_Fetch_NotFound(t *testing.T) {
44 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45 w.WriteHeader(http.StatusNotFound)
46 }))
47 defer server.Close()
48
49 fetcher := NewPDSFetcher(5 * time.Second, 10)
50 ctx := context.Background()
51
52 _, err := fetcher.Fetch(ctx, server.URL, "did:plc:test123", "bafyreicid123")
53 if !errors.Is(err, ErrPDSNotFound) {
54 t.Errorf("expected ErrPDSNotFound, got: %v", err)
55 }
56}
57
58func TestPDSFetcher_Fetch_Timeout(t *testing.T) {
59 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60 // Sleep longer than the timeout
61 time.Sleep(200 * time.Millisecond)
62 w.WriteHeader(http.StatusOK)
63 }))
64 defer server.Close()
65
66 // Use a very short timeout
67 fetcher := NewPDSFetcher(50 * time.Millisecond, 10)
68 ctx := context.Background()
69
70 _, err := fetcher.Fetch(ctx, server.URL, "did:plc:test123", "bafyreicid123")
71 if !errors.Is(err, ErrPDSTimeout) {
72 t.Errorf("expected ErrPDSTimeout, got: %v", err)
73 }
74}
75
76func TestPDSFetcher_Fetch_NetworkError(t *testing.T) {
77 fetcher := NewPDSFetcher(5 * time.Second, 10)
78 ctx := context.Background()
79
80 // Use an invalid URL that will cause a network error
81 _, err := fetcher.Fetch(ctx, "http://localhost:99999", "did:plc:test123", "bafyreicid123")
82 if !errors.Is(err, ErrPDSFetchFailed) {
83 t.Errorf("expected ErrPDSFetchFailed, got: %v", err)
84 }
85}
86
87func TestPDSFetcher_Fetch_ContextCancellation(t *testing.T) {
88 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
89 // Sleep to allow context cancellation
90 time.Sleep(100 * time.Millisecond)
91 w.WriteHeader(http.StatusOK)
92 }))
93 defer server.Close()
94
95 fetcher := NewPDSFetcher(5 * time.Second, 10)
96 ctx, cancel := context.WithCancel(context.Background())
97
98 // Cancel the context immediately
99 cancel()
100
101 _, err := fetcher.Fetch(ctx, server.URL, "did:plc:test123", "bafyreicid123")
102 if err == nil {
103 t.Error("expected error due to context cancellation")
104 }
105 // Context cancellation should return ErrPDSTimeout
106 if !errors.Is(err, ErrPDSTimeout) {
107 t.Errorf("expected ErrPDSTimeout for context cancellation, got: %v", err)
108 }
109}
110
111func TestPDSFetcher_Fetch_ServerError(t *testing.T) {
112 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
113 w.WriteHeader(http.StatusInternalServerError)
114 }))
115 defer server.Close()
116
117 fetcher := NewPDSFetcher(5 * time.Second, 10)
118 ctx := context.Background()
119
120 _, err := fetcher.Fetch(ctx, server.URL, "did:plc:test123", "bafyreicid123")
121 if !errors.Is(err, ErrPDSFetchFailed) {
122 t.Errorf("expected ErrPDSFetchFailed, got: %v", err)
123 }
124}
125
126func TestPDSFetcher_Fetch_URLConstruction(t *testing.T) {
127 var capturedURL string
128 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129 capturedURL = r.URL.String()
130 w.WriteHeader(http.StatusOK)
131 w.Write([]byte("data"))
132 }))
133 defer server.Close()
134
135 fetcher := NewPDSFetcher(5 * time.Second, 10)
136 ctx := context.Background()
137
138 _, err := fetcher.Fetch(ctx, server.URL, "did:plc:abc123", "bafyreicid456")
139 if err != nil {
140 t.Fatalf("unexpected error: %v", err)
141 }
142
143 expectedPath := "/xrpc/com.atproto.sync.getBlob?cid=bafyreicid456&did=did%3Aplc%3Aabc123"
144 if capturedURL != expectedPath {
145 t.Errorf("expected URL %q, got %q", expectedPath, capturedURL)
146 }
147}
148
149func TestPDSFetcher_Fetch_ImageTooLarge_ContentLength(t *testing.T) {
150 // Server returns Content-Length header indicating size exceeds limit
151 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
152 // Set Content-Length larger than the max (1MB)
153 w.Header().Set("Content-Length", "2097152") // 2MB
154 w.WriteHeader(http.StatusOK)
155 // Don't actually write 2MB of data
156 }))
157 defer server.Close()
158
159 // Use 1MB max size
160 fetcher := NewPDSFetcher(5*time.Second, 1)
161 ctx := context.Background()
162
163 _, err := fetcher.Fetch(ctx, server.URL, "did:plc:test123", "bafyreicid123")
164 if !errors.Is(err, ErrImageTooLarge) {
165 t.Errorf("expected ErrImageTooLarge, got: %v", err)
166 }
167}
168
169func TestPDSFetcher_Fetch_ImageTooLarge_StreamingBody(t *testing.T) {
170 // Server doesn't send Content-Length but streams more data than allowed
171 largeData := make([]byte, 2*1024*1024) // 2MB of zeros
172 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
173 w.WriteHeader(http.StatusOK)
174 w.Write(largeData)
175 }))
176 defer server.Close()
177
178 // Use 1MB max size
179 fetcher := NewPDSFetcher(5*time.Second, 1)
180 ctx := context.Background()
181
182 _, err := fetcher.Fetch(ctx, server.URL, "did:plc:test123", "bafyreicid123")
183 if !errors.Is(err, ErrImageTooLarge) {
184 t.Errorf("expected ErrImageTooLarge, got: %v", err)
185 }
186}
187
188func TestPDSFetcher_Fetch_SizeWithinLimit(t *testing.T) {
189 // Server returns data within the limit
190 testData := make([]byte, 512*1024) // 512KB
191 for i := range testData {
192 testData[i] = byte(i % 256)
193 }
194 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
195 w.WriteHeader(http.StatusOK)
196 w.Write(testData)
197 }))
198 defer server.Close()
199
200 // Use 1MB max size
201 fetcher := NewPDSFetcher(5*time.Second, 1)
202 ctx := context.Background()
203
204 data, err := fetcher.Fetch(ctx, server.URL, "did:plc:test123", "bafyreicid123")
205 if err != nil {
206 t.Fatalf("expected no error, got: %v", err)
207 }
208 if len(data) != len(testData) {
209 t.Errorf("expected %d bytes, got %d", len(testData), len(data))
210 }
211}
212
213func TestPDSFetcher_Fetch_DefaultMaxSize(t *testing.T) {
214 // Test that 0 for maxSizeMB uses the default
215 fetcher := NewPDSFetcher(5*time.Second, 0)
216 expectedDefault := int64(DefaultMaxSourceSizeMB) * 1024 * 1024
217
218 if fetcher.maxSizeBytes != expectedDefault {
219 t.Errorf("expected default maxSizeBytes %d, got %d", expectedDefault, fetcher.maxSizeBytes)
220 }
221}
222
223func TestPDSFetcher_Fetch_NegativeMaxSize(t *testing.T) {
224 // Test that negative maxSizeMB uses the default
225 fetcher := NewPDSFetcher(5*time.Second, -5)
226 expectedDefault := int64(DefaultMaxSourceSizeMB) * 1024 * 1024
227
228 if fetcher.maxSizeBytes != expectedDefault {
229 t.Errorf("expected default maxSizeBytes %d, got %d", expectedDefault, fetcher.maxSizeBytes)
230 }
231}