@@ -2,6 +2,7 @@ package main
22
33import (
44 "encoding/json"
5+ "fmt"
56 "net/http"
67 "net/http/httptest"
78 "os"
@@ -405,11 +406,101 @@ func TestVideoDownload_DownloadFails(t *testing.T) {
405406 if res .ExitCode != 1 {
406407 t .Fatalf ("ExitCode = %d, want 1\n stderr: %s" , res .ExitCode , res .Stderr )
407408 }
409+ if ! strings .Contains (res .Stderr , `"code":"cli_download_failed"` ) {
410+ t .Fatalf ("stderr should carry cli_download_failed (asset-host 5xx):\n %s" , res .Stderr )
411+ }
408412 if _ , err := os .Stat (dest ); ! os .IsNotExist (err ) {
409413 t .Fatalf ("expected no output file, stat err = %v" , err )
410414 }
411415}
412416
417+ // A presigned asset URL that 403s or 404s is expired/revoked — a client-side
418+ // cli_download_url_expired, distinct from the asset host failing (cli_download_failed).
419+ func TestVideoDownload_ExpiredURL (t * testing.T ) {
420+ for _ , status := range []int {http .StatusForbidden , http .StatusNotFound } {
421+ t .Run (fmt .Sprintf ("HTTP_%d" , status ), func (t * testing.T ) {
422+ tmpDir := t .TempDir ()
423+ dest := filepath .Join (tmpDir , "expired.mp4" )
424+
425+ var srv * httptest.Server
426+ srv = httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
427+ switch {
428+ case r .Method == http .MethodGet && r .URL .Path == "/v3/videos/vid_123" :
429+ writeJSON (t , w , map [string ]any {
430+ "data" : map [string ]any {
431+ "video_url" : srv .URL + "/download/expired.mp4" ,
432+ "status" : "completed" ,
433+ },
434+ })
435+ case r .Method == http .MethodGet && r .URL .Path == "/download/expired.mp4" :
436+ w .WriteHeader (status )
437+ default :
438+ t .Fatalf ("unexpected request: %s %s" , r .Method , r .URL .Path )
439+ }
440+ }))
441+ defer srv .Close ()
442+
443+ res := runCommand (t , srv .URL , "test-key" , "video" , "download" , "vid_123" , "--output-path" , dest )
444+ if res .ExitCode != 1 {
445+ t .Fatalf ("ExitCode = %d, want 1\n stderr: %s" , res .ExitCode , res .Stderr )
446+ }
447+ if ! strings .Contains (res .Stderr , `"code":"cli_download_url_expired"` ) {
448+ t .Fatalf ("stderr should carry cli_download_url_expired (HTTP %d on presigned URL):\n %s" , status , res .Stderr )
449+ }
450+ if ! strings .Contains (res .Stderr , "video get" ) {
451+ t .Fatalf ("expired-URL hint should point to re-fetching via video get:\n %s" , res .Stderr )
452+ }
453+ })
454+ }
455+ }
456+
457+ // The video-get response failing to parse, or yielding an unusable download URL,
458+ // classifies as cli_response_parse_error (not the opaque error code).
459+ func TestVideoDownload_ResponseParseError (t * testing.T ) {
460+ t .Run ("malformed video-get JSON" , func (t * testing.T ) {
461+ dest := filepath .Join (t .TempDir (), "x.mp4" )
462+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
463+ if r .URL .Path == "/v3/videos/vid_123" {
464+ w .WriteHeader (http .StatusOK )
465+ _ , _ = w .Write ([]byte ("{ this is not valid json" ))
466+ return
467+ }
468+ t .Fatalf ("unexpected request: %s %s" , r .Method , r .URL .Path )
469+ }))
470+ defer srv .Close ()
471+
472+ res := runCommand (t , srv .URL , "test-key" , "video" , "download" , "vid_123" , "--output-path" , dest )
473+ if res .ExitCode != 1 {
474+ t .Fatalf ("ExitCode = %d, want 1\n stderr: %s" , res .ExitCode , res .Stderr )
475+ }
476+ if ! strings .Contains (res .Stderr , `"code":"cli_response_parse_error"` ) {
477+ t .Fatalf ("stderr should carry cli_response_parse_error:\n %s" , res .Stderr )
478+ }
479+ })
480+
481+ t .Run ("unusable download URL" , func (t * testing.T ) {
482+ dest := filepath .Join (t .TempDir (), "x.mp4" )
483+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
484+ if r .URL .Path == "/v3/videos/vid_123" {
485+ writeJSON (t , w , map [string ]any {
486+ "data" : map [string ]any {"video_url" : "http://%zz" , "status" : "completed" },
487+ })
488+ return
489+ }
490+ t .Fatalf ("unexpected request: %s %s" , r .Method , r .URL .Path )
491+ }))
492+ defer srv .Close ()
493+
494+ res := runCommand (t , srv .URL , "test-key" , "video" , "download" , "vid_123" , "--output-path" , dest )
495+ if res .ExitCode != 1 {
496+ t .Fatalf ("ExitCode = %d, want 1\n stderr: %s" , res .ExitCode , res .Stderr )
497+ }
498+ if ! strings .Contains (res .Stderr , `"code":"cli_response_parse_error"` ) {
499+ t .Fatalf ("stderr should carry cli_response_parse_error for an unusable URL:\n %s" , res .Stderr )
500+ }
501+ })
502+ }
503+
413504func TestVideoDownload_AuthRequired (t * testing.T ) {
414505 srv := setupTestServer (t , map [string ]testHandler {})
415506 defer srv .Close ()
@@ -429,3 +520,100 @@ func writeJSON(t *testing.T, w http.ResponseWriter, payload any) {
429520 }
430521 _ , _ = w .Write (raw )
431522}
523+
524+ // Transport failure reaching the asset host classifies as network_error (the
525+ // grandfathered shared transport code, intentionally unprefixed).
526+ func TestVideoDownload_NetworkError (t * testing.T ) {
527+ dead := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {}))
528+ deadURL := dead .URL
529+ dead .Close () // deadURL now refuses connections
530+
531+ dest := filepath .Join (t .TempDir (), "x.mp4" )
532+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
533+ if r .URL .Path == "/v3/videos/vid_123" {
534+ writeJSON (t , w , map [string ]any {
535+ "data" : map [string ]any {"video_url" : deadURL + "/x.mp4" , "status" : "completed" },
536+ })
537+ return
538+ }
539+ t .Fatalf ("unexpected request: %s %s" , r .Method , r .URL .Path )
540+ }))
541+ defer srv .Close ()
542+
543+ res := runCommand (t , srv .URL , "test-key" , "video" , "download" , "vid_123" , "--output-path" , dest )
544+ if res .ExitCode != 1 {
545+ t .Fatalf ("ExitCode = %d, want 1\n stderr: %s" , res .ExitCode , res .Stderr )
546+ }
547+ if ! strings .Contains (res .Stderr , `"code":"network_error"` ) {
548+ t .Fatalf ("stderr should carry network_error (transport failure):\n %s" , res .Stderr )
549+ }
550+ }
551+
552+ // A response that promises more bytes than it delivers, then drops, cuts io.Copy
553+ // off mid-stream and classifies as cli_download_interrupted.
554+ func TestVideoDownload_Interrupted (t * testing.T ) {
555+ dest := filepath .Join (t .TempDir (), "x.mp4" )
556+ var srv * httptest.Server
557+ srv = httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
558+ switch r .URL .Path {
559+ case "/v3/videos/vid_123" :
560+ writeJSON (t , w , map [string ]any {
561+ "data" : map [string ]any {"video_url" : srv .URL + "/download/x.mp4" , "status" : "completed" },
562+ })
563+ case "/download/x.mp4" :
564+ hj , ok := w .(http.Hijacker )
565+ if ! ok {
566+ t .Fatal ("test server does not support hijacking" )
567+ }
568+ conn , buf , err := hj .Hijack ()
569+ if err != nil {
570+ t .Fatalf ("hijack: %v" , err )
571+ }
572+ // Promise 1000 bytes, send 7, then drop the connection → the client's
573+ // io.Copy hits an unexpected EOF.
574+ _ , _ = buf .WriteString ("HTTP/1.1 200 OK\r \n Content-Length: 1000\r \n \r \n partial" )
575+ _ = buf .Flush ()
576+ _ = conn .Close ()
577+ default :
578+ t .Fatalf ("unexpected request: %s %s" , r .Method , r .URL .Path )
579+ }
580+ }))
581+ defer srv .Close ()
582+
583+ res := runCommand (t , srv .URL , "test-key" , "video" , "download" , "vid_123" , "--output-path" , dest )
584+ if res .ExitCode != 1 {
585+ t .Fatalf ("ExitCode = %d, want 1\n stderr: %s" , res .ExitCode , res .Stderr )
586+ }
587+ if ! strings .Contains (res .Stderr , `"code":"cli_download_interrupted"` ) {
588+ t .Fatalf ("stderr should carry cli_download_interrupted (mid-stream cutoff):\n %s" , res .Stderr )
589+ }
590+ }
591+
592+ // A destination whose parent directory does not exist fails temp-file creation
593+ // and classifies as cli_file_io_error.
594+ func TestVideoDownload_FileIOError (t * testing.T ) {
595+ dest := filepath .Join (t .TempDir (), "no-such-dir" , "x.mp4" ) // parent dir absent
596+ var srv * httptest.Server
597+ srv = httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
598+ switch r .URL .Path {
599+ case "/v3/videos/vid_123" :
600+ writeJSON (t , w , map [string ]any {
601+ "data" : map [string ]any {"video_url" : srv .URL + "/download/x.mp4" , "status" : "completed" },
602+ })
603+ case "/download/x.mp4" :
604+ w .WriteHeader (http .StatusOK )
605+ _ , _ = w .Write ([]byte ("video-bytes" ))
606+ default :
607+ t .Fatalf ("unexpected request: %s %s" , r .Method , r .URL .Path )
608+ }
609+ }))
610+ defer srv .Close ()
611+
612+ res := runCommand (t , srv .URL , "test-key" , "video" , "download" , "vid_123" , "--output-path" , dest )
613+ if res .ExitCode != 1 {
614+ t .Fatalf ("ExitCode = %d, want 1\n stderr: %s" , res .ExitCode , res .Stderr )
615+ }
616+ if ! strings .Contains (res .Stderr , `"code":"cli_file_io_error"` ) {
617+ t .Fatalf ("stderr should carry cli_file_io_error (temp-create in missing dir):\n %s" , res .Stderr )
618+ }
619+ }
0 commit comments