✅ Improve auth test
This commit is contained in:
1 parent
4f8659c03a
commit
84a27dcf74
5 files changed
+277
-9
No files matched your search
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user