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