@@ -2,11 +2,13 @@ package xmlrpc
22
33import (
44 "bytes"
5+ "encoding/xml"
56 "fmt"
67 "io"
78 "net/http"
89 "net/http/httptest"
910 "testing"
11+ "time"
1012
1113 "github.com/stretchr/testify/require"
1214)
@@ -254,3 +256,154 @@ func TestClient_Option_SkipUnknownFields(t *testing.T) {
254256 })
255257 }
256258}
259+
260+ // recordingTimeFormatter is a third-party TimeFormatter, proving a single instance is
261+ // used for both directions and that its output is escaped on the way out.
262+ type recordingTimeFormatter struct {
263+ formatted int
264+ parsed int
265+ }
266+
267+ func (f * recordingTimeFormatter ) FormatTime (_ time.Time ) string {
268+ f .formatted ++
269+
270+ return "encoded&value"
271+ }
272+
273+ func (f * recordingTimeFormatter ) ParseTime (_ string ) (time.Time , error ) {
274+ f .parsed ++
275+
276+ return time .Date (2001 , 2 , 3 , 4 , 5 , 6 , 0 , time .UTC ), nil
277+ }
278+
279+ func TestClient_Option_TimeFormat_CustomFormatter (t * testing.T ) {
280+ formatter := & recordingTimeFormatter {}
281+
282+ var body string
283+ ts := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
284+ b , err := io .ReadAll (r .Body )
285+ require .NoError (t , err )
286+ body = string (b )
287+
288+ _ , _ = fmt .Fprintln (w , string (loadTestFile (t , "response_datetime_compact.xml" )))
289+ }))
290+ defer ts .Close ()
291+
292+ c , err := NewClient (ts .URL , TimeFormat (formatter ))
293+ require .NoError (t , err )
294+
295+ resp := & struct { When time.Time }{}
296+ require .NoError (t , c .Call ("test.Method" , & struct { When time.Time }{When : time .Now ()}, resp ))
297+
298+ require .Equal (t , 1 , formatter .formatted , "the same instance must encode" )
299+ require .Equal (t , 1 , formatter .parsed , "the same instance must decode" )
300+ require .Equal (t , 2001 , resp .When .Year ())
301+
302+ // formatter output must be escaped, keeping the request well-formed
303+ require .Contains (t , body , "<dateTime.iso8601>encoded&value</dateTime.iso8601>" )
304+ require .NoError (t , xml .Unmarshal ([]byte (body ), new (struct {})), "request body must be well-formed XML" )
305+ }
306+
307+ func TestClient_Option_TimeFormat_TypedNilFormatter (t * testing.T ) {
308+ // A typed nil is non-nil as an interface - it must not panic
309+ var formatter * LayoutTimeFormatter
310+
311+ serverCalled := false
312+ ts := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
313+ body , err := io .ReadAll (r .Body )
314+ require .NoError (t , err )
315+ require .Contains (t , string (body ), "<dateTime.iso8601>2019-10-11T13:40:30Z</dateTime.iso8601>" )
316+
317+ serverCalled = true
318+ _ , _ = fmt .Fprintln (w , string (loadTestFile (t , "response_datetime_rfc3339.xml" )))
319+ }))
320+ defer ts .Close ()
321+
322+ c , err := NewClient (ts .URL , TimeFormat (formatter ))
323+ require .NoError (t , err )
324+
325+ args := & struct { When time.Time }{When : time .Date (2019 , 10 , 11 , 13 , 40 , 30 , 0 , time .UTC )}
326+ resp := & struct { When time.Time }{}
327+ require .NoError (t , c .Call ("test.Method" , args , resp ))
328+
329+ // decode must go through the same fallback
330+ require .Equal (t , "2019-10-11T13:40:30Z" , resp .When .Format (time .RFC3339 ))
331+ require .True (t , serverCalled , "server must be called" )
332+ }
333+
334+ func TestClient_Option_TimeFormat (t * testing.T ) {
335+ input := time .Date (2019 , 10 , 11 , 13 , 40 , 30 , 0 , time .UTC )
336+
337+ tests := []struct {
338+ name string
339+ opts []Option
340+ expectEncode string
341+ expectDecode string
342+ expectErr bool
343+ }{
344+ {
345+ name : "default formatter" ,
346+ expectEncode : "<dateTime.iso8601>2019-10-11T13:40:30Z</dateTime.iso8601>" ,
347+ // Server responds with the compact form, which RFC3339 cannot parse
348+ expectErr : true ,
349+ },
350+ {
351+ name : "nil formatter falls back to default" ,
352+ opts : []Option {
353+ TimeFormat (nil ),
354+ },
355+ expectEncode : "<dateTime.iso8601>2019-10-11T13:40:30Z</dateTime.iso8601>" ,
356+ expectErr : true ,
357+ },
358+ {
359+ name : "compact formatter" ,
360+ opts : []Option {
361+ TimeFormat (& LayoutTimeFormatter {FormatLayout : LayoutISO8601Compact }),
362+ },
363+ expectEncode : "<dateTime.iso8601>20191011T13:40:30</dateTime.iso8601>" ,
364+ expectDecode : "2019-10-11T13:40:30Z" ,
365+ },
366+ {
367+ name : "permissive parse layouts with compact encoding" ,
368+ opts : []Option {
369+ TimeFormat (& LayoutTimeFormatter {
370+ FormatLayout : LayoutISO8601Compact ,
371+ ParseLayouts : CommonParseLayouts (),
372+ }),
373+ },
374+ expectEncode : "<dateTime.iso8601>20191011T13:40:30</dateTime.iso8601>" ,
375+ expectDecode : "2019-10-11T13:40:30Z" ,
376+ },
377+ }
378+
379+ for _ , tt := range tests {
380+ t .Run (tt .name , func (t * testing.T ) {
381+ serverCalled := false
382+ ts := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
383+ body , err := io .ReadAll (r .Body )
384+ require .NoError (t , err )
385+ require .Contains (t , string (body ), tt .expectEncode )
386+
387+ serverCalled = true
388+ _ , _ = fmt .Fprintln (w , string (loadTestFile (t , "response_datetime_compact.xml" )))
389+ }))
390+ defer ts .Close ()
391+
392+ c , err := NewClient (ts .URL , tt .opts ... )
393+ require .NoError (t , err )
394+
395+ args := & struct { When time.Time }{When : input }
396+ resp := & struct { When time.Time }{}
397+
398+ err = c .Call ("test.Method" , args , resp )
399+ if tt .expectErr {
400+ require .ErrorContains (t , err , "does not match expected time layouts" )
401+ } else {
402+ require .NoError (t , err )
403+ require .Equal (t , tt .expectDecode , resp .When .Format (time .RFC3339 ))
404+ }
405+
406+ require .True (t , serverCalled , "server must be called" )
407+ })
408+ }
409+ }
0 commit comments