@@ -4,12 +4,27 @@ import (
44 "bytes"
55 "context"
66 "encoding/json"
7+ "fmt"
8+ "io"
79 "net/http"
8- "net/http/httptest"
910 "strings"
1011 "testing"
1112)
1213
14+ type roundTripperFunc func (* http.Request ) (* http.Response , error )
15+
16+ func (f roundTripperFunc ) RoundTrip (req * http.Request ) (* http.Response , error ) {
17+ return f (req )
18+ }
19+
20+ func jsonResponse (statusCode int , body string ) * http.Response {
21+ return & http.Response {
22+ StatusCode : statusCode ,
23+ Header : make (http.Header ),
24+ Body : io .NopCloser (strings .NewReader (body )),
25+ }
26+ }
27+
1328func TestStatusPageMigrationAPIStartStructure (t * testing.T ) {
1429 t .Parallel ()
1530
@@ -18,25 +33,21 @@ func TestStatusPageMigrationAPIStartStructure(t *testing.T) {
1833 var gotAppKey string
1934 var gotBody map [string ]any
2035
21- ts := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
22- gotMethod = r .Method
23- gotPath = r .URL .Path
24- gotAppKey = r .URL .Query ().Get ("app_key" )
25- if err := json .NewDecoder (r .Body ).Decode (& gotBody ); err != nil {
26- t .Fatalf ("decode body: %v" , err )
27- }
28- w .Header ().Set ("Content-Type" , "application/json" )
29- _ = json .NewEncoder (w ).Encode (map [string ]any {
30- "data" : map [string ]any {"job_id" : "job-1" },
31- })
32- }))
33- defer ts .Close ()
34-
3536 api := & statusPageMigrationAPI {
36- httpClient : ts .Client (),
37- baseURL : ts .URL ,
38- appKey : "fd-app-key" ,
39- userAgent : "flashduty-cli/test" ,
37+ httpClient : & http.Client {
38+ Transport : roundTripperFunc (func (r * http.Request ) (* http.Response , error ) {
39+ gotMethod = r .Method
40+ gotPath = r .URL .Path
41+ gotAppKey = r .URL .Query ().Get ("app_key" )
42+ if err := json .NewDecoder (r .Body ).Decode (& gotBody ); err != nil {
43+ t .Fatalf ("decode body: %v" , err )
44+ }
45+ return jsonResponse (http .StatusOK , `{"data":{"job_id":"job-1"}}` ), nil
46+ }),
47+ },
48+ baseURL : "https://status.example.com" ,
49+ appKey : "fd-app-key" ,
50+ userAgent : "flashduty-cli/test" ,
4051 }
4152
4253 out , err := api .StartStructure (context .Background (), "atlassian-key" , "page_123" )
@@ -68,32 +79,18 @@ func TestStatusPageMigrationAPIGetStatus(t *testing.T) {
6879 var gotPath string
6980 var gotJobID string
7081
71- ts := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
72- gotMethod = r .Method
73- gotPath = r .URL .Path
74- gotJobID = r .URL .Query ().Get ("job_id" )
75- w .Header ().Set ("Content-Type" , "application/json" )
76- _ = json .NewEncoder (w ).Encode (map [string ]any {
77- "data" : map [string ]any {
78- "job_id" : "job-2" ,
79- "source_page_id" : "src-1" ,
80- "target_page_id" : 1024 ,
81- "phase" : "history" ,
82- "status" : "running" ,
83- "progress" : map [string ]any {
84- "total_steps" : 5 ,
85- "completed_steps" : 3 ,
86- },
87- },
88- })
89- }))
90- defer ts .Close ()
91-
9282 api := & statusPageMigrationAPI {
93- httpClient : ts .Client (),
94- baseURL : ts .URL ,
95- appKey : "fd-app-key" ,
96- userAgent : "flashduty-cli/test" ,
83+ httpClient : & http.Client {
84+ Transport : roundTripperFunc (func (r * http.Request ) (* http.Response , error ) {
85+ gotMethod = r .Method
86+ gotPath = r .URL .Path
87+ gotJobID = r .URL .Query ().Get ("job_id" )
88+ return jsonResponse (http .StatusOK , `{"data":{"job_id":"job-2","source_page_id":"src-1","target_page_id":1024,"phase":"history","status":"running","progress":{"total_steps":5,"completed_steps":3}}}` ), nil
89+ }),
90+ },
91+ baseURL : "https://status.example.com" ,
92+ appKey : "fd-app-key" ,
93+ userAgent : "flashduty-cli/test" ,
9794 }
9895
9996 out , err := api .GetStatus (context .Background (), "job-2" )
@@ -115,29 +112,101 @@ func TestStatusPageMigrationAPIGetStatus(t *testing.T) {
115112 }
116113}
117114
115+ func TestStatusPageMigrationAPIRedactsAppKeyFromTransportError (t * testing.T ) {
116+ t .Parallel ()
117+
118+ api := & statusPageMigrationAPI {
119+ httpClient : & http.Client {
120+ Transport : roundTripperFunc (func (req * http.Request ) (* http.Response , error ) {
121+ return nil , fmt .Errorf ("transport failed for %s" , req .URL .String ())
122+ }),
123+ },
124+ baseURL : "https://status.example.com" ,
125+ appKey : "secret-app-key" ,
126+ userAgent : "flashduty-cli/test" ,
127+ }
128+
129+ _ , err := api .GetStatus (context .Background (), "job-4" )
130+ if err == nil {
131+ t .Fatal ("GetStatus() error = nil, want transport error" )
132+ }
133+ if strings .Contains (err .Error (), "secret-app-key" ) {
134+ t .Fatalf ("transport error leaked app key: %v" , err )
135+ }
136+ }
137+
138+ func TestStatusPageMigrationAPICapsErrorBodyReads (t * testing.T ) {
139+ t .Parallel ()
140+
141+ largeBody := strings .Repeat ("0123456789" , 2000 )
142+
143+ api := & statusPageMigrationAPI {
144+ httpClient : & http.Client {
145+ Transport : roundTripperFunc (func (r * http.Request ) (* http.Response , error ) {
146+ return jsonResponse (http .StatusBadGateway , largeBody ), nil
147+ }),
148+ },
149+ baseURL : "https://status.example.com" ,
150+ appKey : "fd-app-key" ,
151+ userAgent : "flashduty-cli/test" ,
152+ }
153+
154+ _ , err := api .GetStatus (context .Background (), "job-5" )
155+ if err == nil {
156+ t .Fatal ("GetStatus() error = nil, want HTTP error" )
157+ }
158+ if got := len (err .Error ()); got > 5000 {
159+ t .Fatalf ("HTTP error too large: got %d chars, want <= 5000" , got )
160+ }
161+ }
162+
163+ func TestStatusPageMigrationAPISanitizesErrorBodyFields (t * testing.T ) {
164+ t .Parallel ()
165+
166+ api := & statusPageMigrationAPI {
167+ httpClient : & http.Client {
168+ Transport : roundTripperFunc (func (r * http.Request ) (* http.Response , error ) {
169+ return jsonResponse (http .StatusBadGateway , `{"error":{"message":"upstream failed","details":{"ApiKey":"response-secret","nested":[{"ACCESS_TOKEN":"response-token"}]}}}` ), nil
170+ }),
171+ },
172+ baseURL : "https://status.example.com" ,
173+ appKey : "fd-app-key" ,
174+ userAgent : "flashduty-cli/test" ,
175+ }
176+
177+ _ , err := api .GetStatus (context .Background (), "job-6" )
178+ if err == nil {
179+ t .Fatal ("GetStatus() error = nil, want HTTP error" )
180+ }
181+ if strings .Contains (err .Error (), "response-secret" ) || strings .Contains (err .Error (), "response-token" ) {
182+ t .Fatalf ("HTTP error leaked secret body fields: %v" , err )
183+ }
184+ if ! strings .Contains (err .Error (), "[REDACTED]" ) {
185+ t .Fatalf ("HTTP error missing redaction marker: %v" , err )
186+ }
187+ }
188+
118189func TestStatusPageMigrationAPICancel (t * testing.T ) {
119190 t .Parallel ()
120191
121192 var gotMethod string
122193 var gotPath string
123194 var gotBody map [string ]any
124195
125- ts := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
126- gotMethod = r .Method
127- gotPath = r .URL .Path
128- if err := json .NewDecoder (r .Body ).Decode (& gotBody ); err != nil {
129- t .Fatalf ("decode body: %v" , err )
130- }
131- w .Header ().Set ("Content-Type" , "application/json" )
132- _ = json .NewEncoder (w ).Encode (map [string ]any {"data" : map [string ]any {}})
133- }))
134- defer ts .Close ()
135-
136196 api := & statusPageMigrationAPI {
137- httpClient : ts .Client (),
138- baseURL : ts .URL ,
139- appKey : "fd-app-key" ,
140- userAgent : "flashduty-cli/test" ,
197+ httpClient : & http.Client {
198+ Transport : roundTripperFunc (func (r * http.Request ) (* http.Response , error ) {
199+ gotMethod = r .Method
200+ gotPath = r .URL .Path
201+ if err := json .NewDecoder (r .Body ).Decode (& gotBody ); err != nil {
202+ t .Fatalf ("decode body: %v" , err )
203+ }
204+ return jsonResponse (http .StatusOK , `{"data":{}}` ), nil
205+ }),
206+ },
207+ baseURL : "https://status.example.com" ,
208+ appKey : "fd-app-key" ,
209+ userAgent : "flashduty-cli/test" ,
141210 }
142211
143212 if err := api .Cancel (context .Background (), "job-3" ); err != nil {
0 commit comments