411 lines
9.9 KiB
Go
411 lines
9.9 KiB
Go
// Copyright 2026 eat-pray-ai & OpenWaygate
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/jedib0t/go-pretty/v6/table"
|
|
sdkauth "github.com/modelcontextprotocol/go-sdk/auth"
|
|
"google.golang.org/api/youtube/v3"
|
|
)
|
|
|
|
// testResource is a minimal type that satisfies HasFields for option testing.
|
|
type testResource struct {
|
|
Fields
|
|
}
|
|
|
|
func (r *testResource) GetFields() *Fields {
|
|
return &r.Fields
|
|
}
|
|
|
|
func (r *testResource) EnsureService() error {
|
|
return r.Fields.EnsureService()
|
|
}
|
|
|
|
// videoExtract is the extract function for Paginate with youtube.VideoListResponse.
|
|
func videoExtract(r *youtube.VideoListResponse) ([]*youtube.Video, string) {
|
|
return r.Items, r.NextPageToken
|
|
}
|
|
|
|
// ---------- TestPaginate ----------
|
|
|
|
func TestPaginate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
handler http.HandlerFunc
|
|
maxResults int64
|
|
wantLen int
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "single page",
|
|
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"items": [
|
|
{"id": "v1"},
|
|
{"id": "v2"}
|
|
],
|
|
"nextPageToken": ""
|
|
}`))
|
|
}),
|
|
maxResults: 10,
|
|
wantLen: 2,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "multi page",
|
|
handler: func() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
pageToken := r.URL.Query().Get("pageToken")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch pageToken {
|
|
case "":
|
|
items := make([]string, 20)
|
|
for i := range 20 {
|
|
items[i] = fmt.Sprintf(`{"id": "v%d"}`, i+1)
|
|
}
|
|
jsonItems := "[" + strings.Join(items, ",") + "]"
|
|
_, _ = fmt.Fprintf(w, `{
|
|
"items": %s,
|
|
"nextPageToken": "page-2"
|
|
}`, jsonItems)
|
|
case "page-2":
|
|
_, _ = w.Write([]byte(`{
|
|
"items": [{"id": "v21"}, {"id": "v22"}],
|
|
"nextPageToken": ""
|
|
}`))
|
|
}
|
|
}
|
|
}(),
|
|
maxResults: 22,
|
|
wantLen: 22,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "zero results",
|
|
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"items": [],
|
|
"nextPageToken": ""
|
|
}`))
|
|
}),
|
|
maxResults: 10,
|
|
wantLen: 0,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "API error first page",
|
|
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
}),
|
|
maxResults: 10,
|
|
wantLen: 0,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "API error second page",
|
|
handler: func() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
pageToken := r.URL.Query().Get("pageToken")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if pageToken == "" {
|
|
items := make([]string, 20)
|
|
for i := range 20 {
|
|
items[i] = fmt.Sprintf(`{"id": "v%d"}`, i+1)
|
|
}
|
|
jsonItems := "[" + strings.Join(items, ",") + "]"
|
|
_, _ = fmt.Fprintf(w, `{
|
|
"items": %s,
|
|
"nextPageToken": "page-2"
|
|
}`, jsonItems)
|
|
} else {
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}(),
|
|
maxResults: 40,
|
|
wantLen: 20,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "maxResults smaller than page",
|
|
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
items := make([]string, 5)
|
|
for i := range 5 {
|
|
items[i] = fmt.Sprintf(`{"id": "v%d"}`, i+1)
|
|
}
|
|
jsonItems := "[" + strings.Join(items, ",") + "]"
|
|
_, _ = fmt.Fprintf(w, `{
|
|
"items": %s,
|
|
"nextPageToken": ""
|
|
}`, jsonItems)
|
|
}),
|
|
maxResults: 5,
|
|
wantLen: 5,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc := NewTestService(t, tt.handler)
|
|
f := &Fields{
|
|
Service: svc,
|
|
MaxResults: tt.maxResults,
|
|
}
|
|
call := svc.Videos.List([]string{"id"})
|
|
got, err := Paginate(f, call, videoExtract, fmt.Errorf("video list failed"))
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Paginate() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if len(got) != tt.wantLen {
|
|
t.Errorf("Paginate() got %d items, want %d", len(got), tt.wantLen)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------- TestPrintList ----------
|
|
|
|
func TestPrintList(t *testing.T) {
|
|
items := []*youtube.Video{
|
|
{Id: "v1", Snippet: &youtube.VideoSnippet{Title: "Title 1"}},
|
|
{Id: "v2", Snippet: &youtube.VideoSnippet{Title: "Title 2"}},
|
|
}
|
|
header := table.Row{"ID", "Title"}
|
|
rowFn := func(v *youtube.Video) table.Row {
|
|
return table.Row{v.Id, v.Snippet.Title}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
output string
|
|
}{
|
|
{name: "json output", output: "json"},
|
|
{name: "yaml output", output: "yaml"},
|
|
{name: "table output", output: "table"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
PrintList(tt.output, items, &buf, header, rowFn)
|
|
if buf.Len() == 0 {
|
|
t.Errorf("PrintList(%q) produced empty output", tt.output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------- TestPrintResult ----------
|
|
|
|
func TestPrintResult(t *testing.T) {
|
|
data := map[string]string{"id": "v1", "title": "Title 1"}
|
|
|
|
tests := []struct {
|
|
name string
|
|
output string
|
|
wantEmpty bool
|
|
wantSub string
|
|
}{
|
|
{
|
|
name: "json output",
|
|
output: "json",
|
|
wantEmpty: false,
|
|
},
|
|
{
|
|
name: "yaml output",
|
|
output: "yaml",
|
|
wantEmpty: false,
|
|
},
|
|
{
|
|
name: "silent output",
|
|
output: "silent",
|
|
wantEmpty: true,
|
|
},
|
|
{
|
|
name: "default output",
|
|
output: "",
|
|
wantEmpty: false,
|
|
wantSub: "Video v1 updated",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
PrintResult(tt.output, data, &buf, "Video %s updated", "v1")
|
|
if tt.wantEmpty && buf.Len() != 0 {
|
|
t.Errorf("PrintResult(%q) should produce no output, got %q", tt.output, buf.String())
|
|
}
|
|
if !tt.wantEmpty && buf.Len() == 0 {
|
|
t.Errorf("PrintResult(%q) produced empty output", tt.output)
|
|
}
|
|
if tt.wantSub != "" && !strings.Contains(buf.String(), tt.wantSub) {
|
|
t.Errorf("PrintResult(%q) = %q, want substring %q", tt.output, buf.String(), tt.wantSub)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------- TestSetContext ----------
|
|
|
|
func TestSetContext(t *testing.T) {
|
|
f := &Fields{}
|
|
ctx := t.Context()
|
|
f.SetContext(ctx)
|
|
if f.Ctx != ctx {
|
|
t.Error("SetContext did not set context")
|
|
}
|
|
}
|
|
|
|
|
|
// ---------- TestEnsureService ----------
|
|
|
|
func TestEnsureService(t *testing.T) {
|
|
t.Run("service already set", func(t *testing.T) {
|
|
svc := &youtube.Service{}
|
|
f := &Fields{Service: svc}
|
|
err := f.EnsureService()
|
|
if err != nil {
|
|
t.Errorf("EnsureService() error = %v, want nil", err)
|
|
}
|
|
if f.Service != svc {
|
|
t.Errorf("EnsureService() changed Service pointer")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEnsureService_FallsBackWithoutContext(t *testing.T) {
|
|
f := &Fields{}
|
|
err := f.EnsureService()
|
|
if err == nil {
|
|
t.Skip("credentials available in environment, skipping fallback test")
|
|
}
|
|
if err.Error() == "" {
|
|
t.Error("expected non-empty error message")
|
|
}
|
|
}
|
|
|
|
func TestEnsureService_FallsBackWithContextNoToken(t *testing.T) {
|
|
f := &Fields{}
|
|
f.SetContext(t.Context())
|
|
// No TokenInfo in context → should fall back to file auth
|
|
err := f.EnsureService()
|
|
if err == nil {
|
|
t.Skip("credentials available in environment, skipping")
|
|
}
|
|
// Should get credential error, not context/token error
|
|
_ = err
|
|
}
|
|
|
|
func TestTokenInfoFromContext_Nil(t *testing.T) {
|
|
info := sdkauth.TokenInfoFromContext(t.Context())
|
|
if info != nil {
|
|
t.Error("expected nil TokenInfo from plain context")
|
|
}
|
|
}
|
|
|
|
// ---------- Generic option functions ----------
|
|
|
|
func TestWithMaxResults(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input int64
|
|
want int64
|
|
}{
|
|
{name: "positive", input: 42, want: 42},
|
|
{name: "zero becomes MaxInt64", input: 0, want: math.MaxInt64},
|
|
{name: "negative becomes 1", input: -5, want: 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := &testResource{}
|
|
WithMaxResults[*testResource](tt.input)(r)
|
|
if r.MaxResults != tt.want {
|
|
t.Errorf("WithMaxResults(%d) = %d, want %d", tt.input, r.MaxResults, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWithIds(t *testing.T) {
|
|
r := &testResource{}
|
|
ids := []string{"id1", "id2", "id3"}
|
|
WithIds[*testResource](ids)(r)
|
|
if len(r.Ids) != len(ids) {
|
|
t.Fatalf("WithIds() got %d ids, want %d", len(r.Ids), len(ids))
|
|
}
|
|
for i, id := range r.Ids {
|
|
if id != ids[i] {
|
|
t.Errorf("WithIds()[%d] = %q, want %q", i, id, ids[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWithParts(t *testing.T) {
|
|
r := &testResource{}
|
|
parts := []string{"snippet", "contentDetails"}
|
|
WithParts[*testResource](parts)(r)
|
|
if len(r.Parts) != len(parts) {
|
|
t.Fatalf("WithParts() got %d parts, want %d", len(r.Parts), len(parts))
|
|
}
|
|
for i, p := range r.Parts {
|
|
if p != parts[i] {
|
|
t.Errorf("WithParts()[%d] = %q, want %q", i, p, parts[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWithOutput(t *testing.T) {
|
|
r := &testResource{}
|
|
WithOutput[*testResource]("json")(r)
|
|
if r.Output != "json" {
|
|
t.Errorf("WithOutput() = %q, want %q", r.Output, "json")
|
|
}
|
|
}
|
|
|
|
func TestWithService(t *testing.T) {
|
|
r := &testResource{}
|
|
svc := &youtube.Service{}
|
|
WithService[*testResource](svc)(r)
|
|
if r.Service != svc {
|
|
t.Errorf("WithService() did not set service")
|
|
}
|
|
}
|
|
|
|
func TestWithHl(t *testing.T) {
|
|
r := &testResource{}
|
|
WithHl[*testResource]("en")(r)
|
|
if r.Hl != "en" {
|
|
t.Errorf("WithHl() = %q, want %q", r.Hl, "en")
|
|
}
|
|
}
|
|
|
|
func TestWithChannelId(t *testing.T) {
|
|
r := &testResource{}
|
|
WithChannelId[*testResource]("chan-123")(r)
|
|
if r.ChannelId != "chan-123" {
|
|
t.Errorf("WithChannelId() = %q, want %q", r.ChannelId, "chan-123")
|
|
}
|
|
}
|
|
|
|
func TestWithOnBehalfOfContentOwner(t *testing.T) {
|
|
r := &testResource{}
|
|
WithOnBehalfOfContentOwner[*testResource]("owner-456")(r)
|
|
if r.OnBehalfOfContentOwner != "owner-456" {
|
|
t.Errorf("WithOnBehalfOfContentOwner() = %q, want %q", r.OnBehalfOfContentOwner, "owner-456")
|
|
}
|
|
}
|