✅ Improve auth test

This commit is contained in:
OpenWaygate committed 2026-02-01 18:26:53 +08:00
1 parent 4f8659c03a
commit 84a27dcf74
5 files changed
+277 -9

No files matched your search

+8 -1
View File
@@ -20,6 +20,13 @@ go_library(
go_test(
name = "auth_test",
srcs = ["service_test.go"],
srcs = [
"auth_test.go",
"service_test.go",
],
embed = [":auth"],
deps = [
"//pkg",
"@org_golang_x_oauth2//:oauth2",
],
)
+4 -4
View File
@@ -188,9 +188,9 @@ func (s *svc) startWebServer(redirectURL string) (chan string, error) {
}
func (s *svc) getCodeFromPrompt(authURL string) (code string, err error) {
fmt.Printf(openBrowserHint, authURL)
fmt.Print(manualInputHint)
_, err = fmt.Scan(&code)
_, _ = fmt.Fprintf(s.out, openBrowserHint, authURL)
_, _ = fmt.Fprint(s.out, manualInputHint)
_, err = fmt.Fscan(s.in, &code)
if err != nil {
return "", fmt.Errorf("%s: %w", readPromptFailed, err)
}
@@ -211,7 +211,7 @@ func (s *svc) getTokenFromWeb(
var code string
if err := utils.OpenURL(authURL); err == nil {
fmt.Printf(browserOpenedHint, authURL)
_, _ = fmt.Fprintf(s.out, browserOpenedHint, authURL)
code = <-codeCh
}
+217
View File
@@ -0,0 +1,217 @@
package auth
import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"strings"
"testing"
"time"
"github.com/eat-pray-ai/yutu/pkg"
"golang.org/x/oauth2"
)
// helper to build a minimal valid Google OAuth2 credential JSON.
func validCredentialJSON(redirectURL string) string {
cred := map[string]map[string]any{
"web": {
"client_id": "test-client-id",
"project_id": "test-project",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "test-secret",
"redirect_uris": []string{redirectURL},
},
}
b, _ := json.Marshal(cred)
return string(b)
}
func TestGetConfig_Success(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 config.ClientID != "test-client-id" {
t.Errorf(
"unexpected ClientID: got %q, want %q", config.ClientID, "test-client-id",
)
}
if config.ClientSecret != "test-secret" {
t.Errorf(
"unexpected ClientSecret: got %q, want %q", config.ClientSecret,
"test-secret",
)
}
if config.RedirectURL != "http://localhost:8216" {
t.Errorf(
"unexpected RedirectURL: got %q, want %q", config.RedirectURL,
"http://localhost:8216",
)
}
if len(config.Scopes) == 0 {
t.Fatalf("expected scopes to be populated, got none")
}
}
func TestGetConfig_InvalidJSON(t *testing.T) {
s := NewY2BService(
WithCredential("not-a-json", os.DirFS(".")),
).(*svc)
_, err := s.getConfig()
if err == nil {
t.Fatalf("expected error from getConfig with invalid JSON, got nil")
}
if err.Error() == "" {
t.Fatalf("expected non-empty error message")
}
}
func TestStartWebServer_InvalidURL(t *testing.T) {
s := NewY2BService().(*svc)
s.state = "state"
_, err := s.startWebServer("://bad-url")
if err == nil {
t.Fatalf("expected error for invalid URL, got nil")
}
if err.Error() == "" {
t.Fatalf("expected non-empty error message")
}
}
func TestStartWebServer_PortConflict(t *testing.T) {
// Occupy a TCP port first.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen on random port: %v", err)
}
defer func(ln net.Listener) {
_ = ln.Close()
}(ln)
addr := ln.Addr().String()
redirectURL := fmt.Sprintf("http://%s", addr)
s := NewY2BService().(*svc)
s.state = "state"
_, err = s.startWebServer(redirectURL)
if err == nil {
t.Fatalf("expected error due to port conflict, got nil")
}
}
func TestStartWebServer_StateMismatch(t *testing.T) {
// Use an ephemeral port so startWebServer succeeds.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen on random port: %v", err)
}
addr := ln.Addr().String()
_ = ln.Close()
redirectURL := fmt.Sprintf("http://%s", addr)
s := NewY2BService().(*svc)
s.state = "expected-state"
codeCh, err := s.startWebServer(redirectURL)
if err != nil {
t.Fatalf("startWebServer returned error: %v", err)
}
// Send a request with mismatched state.
resp, err := http.Get(redirectURL + "/?state=wrong&code=code123")
if err != nil {
t.Fatalf("failed to send HTTP request: %v", err)
}
_ = resp.Body.Close()
select {
case <-codeCh:
t.Fatalf("expected no code to be sent on state mismatch")
case <-time.After(100 * time.Millisecond):
// ok: channel should remain empty
}
}
func TestGetCodeFromPrompt_Success(t *testing.T) {
var out bytes.Buffer
in := strings.NewReader("some-code")
s := NewY2BService(WithIO(in, &out)).(*svc)
code, err := s.getCodeFromPrompt("http://example.com/auth")
if err != nil {
t.Fatalf("getCodeFromPrompt returned error: %v", err)
}
if code != "some-code" {
t.Fatalf("unexpected code: got %q, want %q", code, "some-code")
}
outStr := out.String()
if !strings.Contains(outStr, openBrowserHint[:20]) { // basic sanity check that hint was written
t.Fatalf("expected openBrowserHint to be written to out, got %q", outStr)
}
if !strings.Contains(outStr, "After completing the authorization flow") {
t.Fatalf("expected manualInputHint to be written to out, got %q", outStr)
}
}
func TestGetCodeFromPrompt_URLDecode(t *testing.T) {
var out bytes.Buffer
in := strings.NewReader("4%2Fabc")
s := NewY2BService(WithIO(in, &out)).(*svc)
code, err := s.getCodeFromPrompt("http://example.com/auth")
if err != nil {
t.Fatalf("getCodeFromPrompt returned error: %v", err)
}
if code != "4/abc" {
t.Fatalf("unexpected code: got %q, want %q", code, "4/abc")
}
}
func TestSaveToken_InvalidPath(t *testing.T) {
// Point pkg.Root to a directory without write permission so that
// the underlying filesystem operations fail.
tmpDir := t.TempDir()
if err := os.Chmod(tmpDir, 0o555); err != nil {
t.Fatalf("failed to chmod temp dir: %v", err)
}
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: "abc"}
err = s.saveToken(tok)
if err == nil {
t.Fatalf("expected error from saveToken with invalid path, got nil")
}
}
+12
View File
@@ -7,6 +7,7 @@ import (
"context"
"encoding/base64"
"fmt"
"io"
"io/fs"
"log/slog"
"os"
@@ -30,6 +31,8 @@ type svc struct {
credFile string
tokenFile string
initErr error
in io.Reader
out io.Writer
service *youtube.Service
ctx context.Context
@@ -47,6 +50,8 @@ func NewY2BService(opts ...Option) Svc {
s.ctx = context.Background()
s.credFile = "client_secret.json"
s.state = utils.RandomStage()
s.in = os.Stdin
s.out = os.Stdout
for _, opt := range opts {
opt(s)
@@ -54,6 +59,13 @@ func NewY2BService(opts ...Option) Svc {
return s
}
func WithIO(in io.Reader, out io.Writer) Option {
return func(s *svc) {
s.in = in
s.out = out
}
}
func WithCredential(cred string, fsys fs.FS) Option {
return func(s *svc) {
// cred > YUTU_CREDENTIAL
+36 -4
View File
@@ -4,11 +4,13 @@
package auth
import (
"bytes"
"context"
"io/fs"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"testing/fstest"
)
@@ -31,6 +33,8 @@ func TestNewY2BService(t *testing.T) {
credFile: &fstest.MapFile{Data: []byte(credential)},
tokenFile: &fstest.MapFile{Data: []byte(cacheToken)},
}
reader := strings.NewReader("in")
writer := &bytes.Buffer{}
type args struct {
opts []Option
@@ -47,6 +51,7 @@ func TestNewY2BService(t *testing.T) {
opts: []Option{
WithCredential(credential, mockFS),
WithCacheToken(cacheToken, mockFS),
WithIO(reader, writer),
},
},
want: &svc{
@@ -54,6 +59,8 @@ func TestNewY2BService(t *testing.T) {
CacheToken: cacheToken,
credFile: credFile,
ctx: context.Background(),
in: reader,
out: writer,
},
},
{
@@ -62,6 +69,7 @@ func TestNewY2BService(t *testing.T) {
opts: []Option{
WithCredential(credB64, mockFS),
WithCacheToken(tokenB64, mockFS),
WithIO(reader, writer),
},
},
want: &svc{
@@ -69,6 +77,8 @@ func TestNewY2BService(t *testing.T) {
CacheToken: cacheToken,
credFile: credFile,
ctx: context.Background(),
in: reader,
out: writer,
},
},
{
@@ -77,6 +87,7 @@ func TestNewY2BService(t *testing.T) {
opts: []Option{
WithCredential(absCred, mockFS),
WithCacheToken(absToken, mockFS),
WithIO(reader, writer),
},
},
want: &svc{
@@ -85,6 +96,8 @@ func TestNewY2BService(t *testing.T) {
credFile: absCred,
tokenFile: tokenFile,
ctx: context.Background(),
in: reader,
out: writer,
},
},
{
@@ -95,6 +108,8 @@ func TestNewY2BService(t *testing.T) {
want: &svc{
credFile: credFile,
ctx: context.Background(),
in: os.Stdin,
out: os.Stdout,
},
},
}
@@ -102,12 +117,29 @@ func TestNewY2BService(t *testing.T) {
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
if got := NewY2BService(tt.args.opts...); !reflect.DeepEqual(
got, tt.want,
) {
t.Errorf("NewY2BService() = %v, want %v", got, tt.want)
got := NewY2BService(tt.args.opts...).(*svc)
got.state = ""
want := tt.want.(*svc)
want.state = ""
if !reflect.DeepEqual(got, want) {
t.Errorf("NewY2BService() = %v, want %v", got, want)
}
},
)
}
}
func TestGetService_Errors(t *testing.T) {
fsys := fstest.MapFS{}
s := NewY2BService(WithCredential("not-a-valid-json-or-b64", fsys)).(*svc)
_, err := s.GetService()
if err == nil {
t.Fatalf("GetService() error = nil, want non-nil")
}
if !strings.Contains(
err.Error(), parseSecretFailed,
) && !strings.Contains(err.Error(), "failed to parse client secret") {
t.Fatalf("GetService() error = %v, want contains %q", err, parseSecretFailed)
}
}