@@ -758,6 +758,249 @@ func TestYouTubeVideosListMyRatingUsesOAuthService(t *testing.T) {
758758 }
759759}
760760
761+ // youtubePartValues collects the videos.list "part" selector from a request.
762+ // The Google SDK may send part either comma-joined or as repeated query params,
763+ // so normalize both into a flat slice.
764+ func youtubePartValues (r * http.Request ) []string {
765+ values := r .URL .Query ()["part" ]
766+ out := make ([]string , 0 , len (values ))
767+ for _ , raw := range values {
768+ out = append (out , strings .Split (raw , "," )... )
769+ }
770+ return out
771+ }
772+
773+ func TestYouTubeVideosListPreservesDefaultParts (t * testing.T ) {
774+ var gotParts []string
775+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
776+ gotParts = youtubePartValues (r )
777+ _ = json .NewEncoder (w ).Encode (map [string ]any {"items" : []map [string ]any {}})
778+ }))
779+ defer srv .Close ()
780+
781+ svc := newGoogleTestServiceWithEndpoint (t , srv .Client (), srv .URL + "/" , youtube .NewService )
782+ ctx := withYouTubeTestServices (newCmdRuntimeOutputContext (t , io .Discard , io .Discard ), youtubeTestServices {
783+ Account : fixedYouTubeTestService (svc ),
784+ })
785+ err := runKong (
t ,
& YouTubeVideosListCmd {}, []
string {
"--id" ,
"vid1" ,
"--max" ,
"1" },
ctx ,
& RootFlags {
Account :
"[email protected] " })
786+ if err != nil {
787+ t .Fatalf ("runKong: %v" , err )
788+ }
789+
790+ want := []string {"snippet" , "contentDetails" , "statistics" }
791+ if strings .Join (gotParts , "," ) != strings .Join (want , "," ) {
792+ t .Fatalf ("parts = %v, want %v" , gotParts , want )
793+ }
794+ }
795+
796+ func TestYouTubeVideosListRequestsAllNonOwnerParts (t * testing.T ) {
797+ var gotParts []string
798+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
799+ if r .URL .Path != "/youtube/v3/videos" {
800+ t .Fatalf ("path = %s" , r .URL .Path )
801+ }
802+ gotParts = youtubePartValues (r )
803+ _ = json .NewEncoder (w ).Encode (map [string ]any {"items" : []map [string ]any {}})
804+ }))
805+ defer srv .Close ()
806+
807+ svc := newGoogleTestServiceWithEndpoint (t , srv .Client (), srv .URL + "/" , youtube .NewService )
808+ ctx := withYouTubeTestServices (newCmdRuntimeOutputContext (t , io .Discard , io .Discard ), youtubeTestServices {
809+ Account : fixedYouTubeTestService (svc ),
810+ APIKey : unexpectedYouTubeTestService (t , "API key service should not be used when account is configured" ),
811+ })
812+ err := runKong (
t ,
& YouTubeVideosListCmd {}, []
string {
"--id" ,
"vid1" ,
"--parts" ,
"all" ,
"--max" ,
"1" },
ctx ,
& RootFlags {
Account :
"[email protected] " })
813+ if err != nil {
814+ t .Fatalf ("runKong: %v" , err )
815+ }
816+
817+ wantParts := []string {
818+ "contentDetails" , "id" , "liveStreamingDetails" , "localizations" ,
819+ "paidProductPlacementDetails" , "player" , "recordingDetails" , "snippet" ,
820+ "statistics" , "status" , "topicDetails" ,
821+ }
822+ if strings .Join (gotParts , "," ) != strings .Join (wantParts , "," ) {
823+ t .Fatalf ("parts = %v, want %v" , gotParts , wantParts )
824+ }
825+ gotSet := make (map [string ]bool , len (gotParts ))
826+ for _ , part := range gotParts {
827+ gotSet [part ] = true
828+ }
829+ for _ , owner := range []string {"fileDetails" , "processingDetails" , "suggestions" } {
830+ if gotSet [owner ] {
831+ t .Fatalf ("part list %v must not request owner-only part %q" , gotParts , owner )
832+ }
833+ }
834+ }
835+
836+ func TestYouTubeVideosListPartsOverride (t * testing.T ) {
837+ var gotParts []string
838+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
839+ if r .URL .Path != "/youtube/v3/videos" {
840+ t .Fatalf ("path = %s" , r .URL .Path )
841+ }
842+ gotParts = youtubePartValues (r )
843+ _ = json .NewEncoder (w ).Encode (map [string ]any {"items" : []map [string ]any {}})
844+ }))
845+ defer srv .Close ()
846+
847+ svc := newGoogleTestServiceWithEndpoint (t , srv .Client (), srv .URL + "/" , youtube .NewService )
848+ ctx := withYouTubeTestServices (newCmdRuntimeOutputContext (t , io .Discard , io .Discard ), youtubeTestServices {
849+ Account : fixedYouTubeTestService (svc ),
850+ })
851+ err := runKong (
t ,
& YouTubeVideosListCmd {}, []
string {
"--id" ,
"vid1" ,
"--parts" ,
"snippet, fileDetails" ,
"--max" ,
"1" },
ctx ,
& RootFlags {
Account :
"[email protected] " })
852+ if err != nil {
853+ t .Fatalf ("runKong: %v" , err )
854+ }
855+ if len (gotParts ) != 2 || gotParts [0 ] != "snippet" || gotParts [1 ] != "fileDetails" {
856+ t .Fatalf ("parts = %v, want [snippet fileDetails]" , gotParts )
857+ }
858+ }
859+
860+ func TestYouTubeVideosListRejectsMixedAllParts (t * testing.T ) {
861+ ctx := withYouTubeTestServices (newCmdRuntimeOutputContext (t , io .Discard , io .Discard ), youtubeTestServices {
862+ Account : unexpectedYouTubeTestService (t , "invalid --parts must not create a service" ),
863+ APIKey : unexpectedYouTubeTestService (t , "invalid --parts must not create a service" ),
864+ })
865+ err := runKong (
t ,
& YouTubeVideosListCmd {}, []
string {
"--id" ,
"vid1" ,
"--parts" ,
"all,status" ,
"--max" ,
"1" },
ctx ,
& RootFlags {
Account :
"[email protected] " })
866+ if err == nil || ExitCode (err ) != 2 || ! strings .Contains (err .Error (), "--parts all cannot be combined with explicit parts" ) {
867+ t .Fatalf ("expected mixed all usage error, got %v" , err )
868+ }
869+ }
870+
871+ func TestYouTubeVideosListJSONSerializesNonCoreParts (t * testing.T ) {
872+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
873+ if r .URL .Path != "/youtube/v3/videos" {
874+ t .Fatalf ("path = %s" , r .URL .Path )
875+ }
876+ _ = json .NewEncoder (w ).Encode (map [string ]any {
877+ "items" : []map [string ]any {
878+ {
879+ "id" : "vidRich" ,
880+ "snippet" : map [string ]any {
881+ "title" : "Rich Video" ,
882+ "publishedAt" : "2026-01-02T03:04:05Z" ,
883+ "thumbnails" : map [string ]any {
884+ "default" : map [string ]any {"url" : "https://img/d.jpg" , "width" : 120 , "height" : 90 },
885+ "high" : map [string ]any {"url" : "https://img/h.jpg" , "width" : 480 , "height" : 360 },
886+ "maxres" : map [string ]any {"url" : "https://img/m.jpg" , "width" : 1280 , "height" : 720 },
887+ },
888+ },
889+ "status" : map [string ]any {
890+ "privacyStatus" : "public" ,
891+ "uploadStatus" : "processed" ,
892+ "madeForKids" : false ,
893+ },
894+ "topicDetails" : map [string ]any {
895+ "topicCategories" : []string {"https://en.wikipedia.org/wiki/Music" },
896+ },
897+ "liveStreamingDetails" : map [string ]any {
898+ "actualStartTime" : "2026-01-01T00:00:00Z" ,
899+ },
900+ "paidProductPlacementDetails" : map [string ]any {
901+ "hasPaidProductPlacement" : true ,
902+ },
903+ },
904+ },
905+ })
906+ }))
907+ defer srv .Close ()
908+
909+ svc := newGoogleTestServiceWithEndpoint (t , srv .Client (), srv .URL + "/" , youtube .NewService )
910+ var stdout bytes.Buffer
911+ ctx := withYouTubeTestServices (newCmdRuntimeJSONOutputContext (t , & stdout , io .Discard ), youtubeTestServices {
912+ Account : fixedYouTubeTestService (svc ),
913+ })
914+ err := runKong (
t ,
& YouTubeVideosListCmd {}, []
string {
"--id" ,
"vidRich" ,
"--parts" ,
"all" ,
"--max" ,
"1" },
ctx ,
& RootFlags {
Account :
"[email protected] " ,
JSON :
true })
915+ if err != nil {
916+ t .Fatalf ("runKong: %v" , err )
917+ }
918+
919+ var got struct {
920+ Items []struct {
921+ ID string `json:"id"`
922+ Status struct {
923+ PrivacyStatus string `json:"privacyStatus"`
924+ UploadStatus string `json:"uploadStatus"`
925+ } `json:"status"`
926+ TopicDetails struct {
927+ TopicCategories []string `json:"topicCategories"`
928+ } `json:"topicDetails"`
929+ Snippet struct {
930+ Thumbnails map [string ]struct {
931+ URL string `json:"url"`
932+ } `json:"thumbnails"`
933+ } `json:"snippet"`
934+ LiveStreamingDetails struct {
935+ ActualStartTime string `json:"actualStartTime"`
936+ } `json:"liveStreamingDetails"`
937+ PaidProductPlacementDetails struct {
938+ HasPaidProductPlacement bool `json:"hasPaidProductPlacement"`
939+ } `json:"paidProductPlacementDetails"`
940+ } `json:"items"`
941+ }
942+ if err := json .Unmarshal (stdout .Bytes (), & got ); err != nil {
943+ t .Fatalf ("json output %q: %v" , stdout .String (), err )
944+ }
945+ if len (got .Items ) != 1 {
946+ t .Fatalf ("items len = %d: %s" , len (got .Items ), stdout .String ())
947+ }
948+ item := got .Items [0 ]
949+ if item .Status .PrivacyStatus != "public" {
950+ t .Fatalf ("status.privacyStatus = %q (non-core status part dropped): %s" , item .Status .PrivacyStatus , stdout .String ())
951+ }
952+ if len (item .TopicDetails .TopicCategories ) != 1 {
953+ t .Fatalf ("topicDetails.topicCategories = %v (non-core topicDetails part dropped): %s" , item .TopicDetails .TopicCategories , stdout .String ())
954+ }
955+ for _ , size := range []string {"default" , "high" , "maxres" } {
956+ if item .Snippet .Thumbnails [size ].URL == "" {
957+ t .Fatalf ("thumbnail size %q missing from JSON (compacted): %s" , size , stdout .String ())
958+ }
959+ }
960+ if item .LiveStreamingDetails .ActualStartTime == "" {
961+ t .Fatalf ("liveStreamingDetails.actualStartTime dropped: %s" , stdout .String ())
962+ }
963+ if ! item .PaidProductPlacementDetails .HasPaidProductPlacement {
964+ t .Fatalf ("paidProductPlacementDetails.hasPaidProductPlacement dropped: %s" , stdout .String ())
965+ }
966+ }
967+
968+ // A video with no liveStreamingDetails (a normal non-live video) must still
969+ // serialize cleanly — the SDK omits parts with no data, never errors.
970+ func TestYouTubeVideosListToleratesPartialParts (t * testing.T ) {
971+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
972+ _ = json .NewEncoder (w ).Encode (map [string ]any {
973+ "items" : []map [string ]any {
974+ {
975+ "id" : "vidPlain" ,
976+ "snippet" : map [string ]any {"title" : "Plain Video" },
977+ "status" : map [string ]any {"privacyStatus" : "unlisted" },
978+ },
979+ },
980+ })
981+ }))
982+ defer srv .Close ()
983+
984+ svc := newGoogleTestServiceWithEndpoint (t , srv .Client (), srv .URL + "/" , youtube .NewService )
985+ var stdout bytes.Buffer
986+ ctx := withYouTubeTestServices (newCmdRuntimeJSONOutputContext (t , & stdout , io .Discard ), youtubeTestServices {
987+ Account : fixedYouTubeTestService (svc ),
988+ })
989+ err := runKong (
t ,
& YouTubeVideosListCmd {}, []
string {
"--id" ,
"vidPlain" ,
"--parts" ,
"all" ,
"--max" ,
"1" },
ctx ,
& RootFlags {
Account :
"[email protected] " ,
JSON :
true })
990+ if err != nil {
991+ t .Fatalf ("runKong: %v" , err )
992+ }
993+ var got struct {
994+ Items []json.RawMessage `json:"items"`
995+ }
996+ if err := json .Unmarshal (stdout .Bytes (), & got ); err != nil {
997+ t .Fatalf ("json output %q: %v" , stdout .String (), err )
998+ }
999+ if len (got .Items ) != 1 {
1000+ t .Fatalf ("items len = %d: %s" , len (got .Items ), stdout .String ())
1001+ }
1002+ }
1003+
7611004func TestYouTubeVideosListMyRatingValidation (t * testing.T ) {
7621005 ctx := withYouTubeTestServices (newCmdRuntimeOutputContext (t , io .Discard , io .Discard ), youtubeTestServices {
7631006 Account : unexpectedYouTubeTestService (t , "should not reach service with invalid my-rating" ),
0 commit comments