✅ Add tests for common

This commit is contained in:
OpenWaygate committed 2026-04-07 20:54:06 +08:00
1 parent befcf78c36
commit a0c03f986a
6 files changed
+524

No files matched your search

+119
View File
@@ -12,10 +12,12 @@ import (
"os"
"strings"
"testing"
"testing/iotest"
"time"
"github.com/eat-pray-ai/yutu/pkg"
"golang.org/x/oauth2"
"google.golang.org/api/youtube/v3"
)
// helper to build a minimal valid Google OAuth2 credential JSON.
@@ -227,3 +229,120 @@ func TestSaveToken_InvalidPath(t *testing.T) {
t.Fatalf("expected error from saveToken with invalid path, got nil")
}
}
func TestStartWebServer_Success(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
addr := ln.Addr().String()
_ = ln.Close()
redirectURL := fmt.Sprintf("http://%s", addr)
s := NewY2BService().(*svc)
s.state = "test-state"
codeCh, err := s.startWebServer(redirectURL)
if err != nil {
t.Fatalf("startWebServer error: %v", err)
}
// Send the HTTP request in a goroutine to avoid deadlock:
// the handler blocks on codeCh <- code until someone reads from codeCh.
go func() {
resp, err := http.Get(redirectURL + "/?state=test-state&code=auth-code-123")
if err == nil {
_ = resp.Body.Close()
}
}()
select {
case code := <-codeCh:
if code != "auth-code-123" {
t.Errorf("expected code=auth-code-123, got %s", code)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for code")
}
}
func TestSaveToken_Success(t *testing.T) {
tmpDir := t.TempDir()
origRootDir := *pkg.RootDir
origRoot := pkg.Root
defer func() {
pkg.RootDir = &origRootDir
pkg.Root = origRoot
}()
pkg.RootDir = &tmpDir
root, err := os.OpenRoot(tmpDir)
if err != nil {
t.Fatalf("failed to open root: %v", err)
}
pkg.Root = root
s := NewY2BService().(*svc)
s.tokenFile = "token.json"
tok := &oauth2.Token{AccessToken: "test-token"}
err = s.saveToken(tok)
if err != nil {
t.Fatalf("saveToken returned error: %v", err)
}
data, err := os.ReadFile(tmpDir + "/token.json")
if err != nil {
t.Fatalf("failed to read saved token file: %v", err)
}
var savedToken oauth2.Token
if err := json.Unmarshal(data, &savedToken); err != nil {
t.Fatalf("failed to unmarshal saved token: %v", err)
}
if savedToken.AccessToken != "test-token" {
t.Errorf("expected AccessToken=test-token, got %s", savedToken.AccessToken)
}
}
func TestGetConfig_Scopes(t *testing.T) {
s := NewY2BService(
WithCredential(validCredentialJSON("http://localhost:8216"), os.DirFS(".")),
).(*svc)
config, err := s.getConfig()
if err != nil {
t.Fatalf("getConfig returned error: %v", err)
}
if len(config.Scopes) != 3 {
t.Fatalf("expected 3 scopes, got %d", len(config.Scopes))
}
expectedScopes := []string{
youtube.YoutubeScope,
youtube.YoutubeForceSslScope,
youtube.YoutubeChannelMembershipsCreatorScope,
}
for i, expected := range expectedScopes {
if config.Scopes[i] != expected {
t.Errorf("scope[%d] = %q, want %q", i, config.Scopes[i], expected)
}
}
}
func TestGetCodeFromPrompt_ReadError(t *testing.T) {
errReader := iotest.ErrReader(fmt.Errorf("read error"))
var out bytes.Buffer
s := NewY2BService(WithIO(errReader, &out)).(*svc)
_, err := s.getCodeFromPrompt("http://example.com/auth", "http://localhost:8216")
if err == nil {
t.Fatalf("expected error from getCodeFromPrompt, got nil")
}
if !strings.Contains(err.Error(), "failed to read prompt") {
t.Errorf("expected error to contain %q, got %q", "failed to read prompt", err.Error())
}
}
+366
View File
@@ -0,0 +1,366 @@
// 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"
"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")
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 if pageToken == "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)
}
})
}
}
// ---------- 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")
}
})
}
// ---------- 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.Fields.MaxResults != tt.want {
t.Errorf("WithMaxResults(%d) = %d, want %d", tt.input, r.Fields.MaxResults, tt.want)
}
})
}
}
func TestWithIds(t *testing.T) {
r := &testResource{}
ids := []string{"id1", "id2", "id3"}
WithIds[*testResource](ids)(r)
if len(r.Fields.Ids) != len(ids) {
t.Fatalf("WithIds() got %d ids, want %d", len(r.Fields.Ids), len(ids))
}
for i, id := range r.Fields.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.Fields.Parts) != len(parts) {
t.Fatalf("WithParts() got %d parts, want %d", len(r.Fields.Parts), len(parts))
}
for i, p := range r.Fields.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.Fields.Output != "json" {
t.Errorf("WithOutput() = %q, want %q", r.Fields.Output, "json")
}
}
func TestWithService(t *testing.T) {
r := &testResource{}
svc := &youtube.Service{}
WithService[*testResource](svc)(r)
if r.Fields.Service != svc {
t.Errorf("WithService() did not set service")
}
}
func TestWithHl(t *testing.T) {
r := &testResource{}
WithHl[*testResource]("en")(r)
if r.Fields.Hl != "en" {
t.Errorf("WithHl() = %q, want %q", r.Fields.Hl, "en")
}
}
func TestWithChannelId(t *testing.T) {
r := &testResource{}
WithChannelId[*testResource]("chan-123")(r)
if r.Fields.ChannelId != "chan-123" {
t.Errorf("WithChannelId() = %q, want %q", r.Fields.ChannelId, "chan-123")
}
}
func TestWithOnBehalfOfContentOwner(t *testing.T) {
r := &testResource{}
WithOnBehalfOfContentOwner[*testResource]("owner-456")(r)
if r.Fields.OnBehalfOfContentOwner != "owner-456" {
t.Errorf("WithOnBehalfOfContentOwner() = %q, want %q", r.Fields.OnBehalfOfContentOwner, "owner-456")
}
}
+4
View File
@@ -17,6 +17,10 @@ func TestInitLogger(t *testing.T) {
{"info", "info"},
{"warn", "warn"},
{"error", "error"},
{"DEBUG uppercase", "DEBUG"},
{"INFO uppercase", "INFO"},
{"WARN uppercase", "WARN"},
{"ERROR uppercase", "ERROR"},
{"default", ""},
}
+31
View File
@@ -43,6 +43,16 @@ func TestStrToBoolPtr(t *testing.T) {
args: args{b: new("")},
want: nil,
},
{
name: "null",
args: args{b: new("null")},
want: nil,
},
{
name: "NULL",
args: args{b: new("NULL")},
want: nil,
},
}
for _, tt := range tests {
t.Run(
@@ -74,6 +84,16 @@ func TestGetFileName(t *testing.T) {
args: args{file: "example"},
want: "example",
},
{
name: "with path",
args: args{file: "/foo/bar/example.txt"},
want: "example",
},
{
name: "double extension",
args: args{file: "archive.tar.gz"},
want: "archive.tar",
},
}
for _, tt := range tests {
t.Run(
@@ -300,6 +320,17 @@ func TestBoolToStrPtr(t *testing.T) {
}
}
func TestRandomStage(t *testing.T) {
s1 := RandomStage()
s2 := RandomStage()
if s1 == "" {
t.Error("RandomStage() returned empty string")
}
if s1 == s2 {
t.Error("RandomStage() returned same value twice")
}
}
func TestHandleCmdError(t *testing.T) {
tests := []struct {
name string