@@ -12,10 +12,12 @@ import (
1212 "os"
1313 "strings"
1414 "testing"
15+ "testing/iotest"
1516 "time"
1617
1718 "github.com/eat-pray-ai/yutu/pkg"
1819 "golang.org/x/oauth2"
20+ "google.golang.org/api/youtube/v3"
1921)
2022
2123// helper to build a minimal valid Google OAuth2 credential JSON.
@@ -227,3 +229,120 @@ func TestSaveToken_InvalidPath(t *testing.T) {
227229 t .Fatalf ("expected error from saveToken with invalid path, got nil" )
228230 }
229231}
232+
233+ func TestStartWebServer_Success (t * testing.T ) {
234+ ln , err := net .Listen ("tcp" , "127.0.0.1:0" )
235+ if err != nil {
236+ t .Fatalf ("failed to listen: %v" , err )
237+ }
238+ addr := ln .Addr ().String ()
239+ _ = ln .Close ()
240+
241+ redirectURL := fmt .Sprintf ("http://%s" , addr )
242+ s := NewY2BService ().(* svc )
243+ s .state = "test-state"
244+
245+ codeCh , err := s .startWebServer (redirectURL )
246+ if err != nil {
247+ t .Fatalf ("startWebServer error: %v" , err )
248+ }
249+
250+ // Send the HTTP request in a goroutine to avoid deadlock:
251+ // the handler blocks on codeCh <- code until someone reads from codeCh.
252+ go func () {
253+ resp , err := http .Get (redirectURL + "/?state=test-state&code=auth-code-123" )
254+ if err == nil {
255+ _ = resp .Body .Close ()
256+ }
257+ }()
258+
259+ select {
260+ case code := <- codeCh :
261+ if code != "auth-code-123" {
262+ t .Errorf ("expected code=auth-code-123, got %s" , code )
263+ }
264+ case <- time .After (2 * time .Second ):
265+ t .Fatal ("timed out waiting for code" )
266+ }
267+ }
268+
269+ func TestSaveToken_Success (t * testing.T ) {
270+ tmpDir := t .TempDir ()
271+
272+ origRootDir := * pkg .RootDir
273+ origRoot := pkg .Root
274+ defer func () {
275+ pkg .RootDir = & origRootDir
276+ pkg .Root = origRoot
277+ }()
278+
279+ pkg .RootDir = & tmpDir
280+ root , err := os .OpenRoot (tmpDir )
281+ if err != nil {
282+ t .Fatalf ("failed to open root: %v" , err )
283+ }
284+ pkg .Root = root
285+
286+ s := NewY2BService ().(* svc )
287+ s .tokenFile = "token.json"
288+
289+ tok := & oauth2.Token {AccessToken : "test-token" }
290+ err = s .saveToken (tok )
291+ if err != nil {
292+ t .Fatalf ("saveToken returned error: %v" , err )
293+ }
294+
295+ data , err := os .ReadFile (tmpDir + "/token.json" )
296+ if err != nil {
297+ t .Fatalf ("failed to read saved token file: %v" , err )
298+ }
299+
300+ var savedToken oauth2.Token
301+ if err := json .Unmarshal (data , & savedToken ); err != nil {
302+ t .Fatalf ("failed to unmarshal saved token: %v" , err )
303+ }
304+ if savedToken .AccessToken != "test-token" {
305+ t .Errorf ("expected AccessToken=test-token, got %s" , savedToken .AccessToken )
306+ }
307+ }
308+
309+ func TestGetConfig_Scopes (t * testing.T ) {
310+ s := NewY2BService (
311+ WithCredential (validCredentialJSON ("http://localhost:8216" ), os .DirFS ("." )),
312+ ).(* svc )
313+
314+ config , err := s .getConfig ()
315+ if err != nil {
316+ t .Fatalf ("getConfig returned error: %v" , err )
317+ }
318+
319+ if len (config .Scopes ) != 3 {
320+ t .Fatalf ("expected 3 scopes, got %d" , len (config .Scopes ))
321+ }
322+
323+ expectedScopes := []string {
324+ youtube .YoutubeScope ,
325+ youtube .YoutubeForceSslScope ,
326+ youtube .YoutubeChannelMembershipsCreatorScope ,
327+ }
328+ for i , expected := range expectedScopes {
329+ if config .Scopes [i ] != expected {
330+ t .Errorf ("scope[%d] = %q, want %q" , i , config .Scopes [i ], expected )
331+ }
332+ }
333+ }
334+
335+ func TestGetCodeFromPrompt_ReadError (t * testing.T ) {
336+ errReader := iotest .ErrReader (fmt .Errorf ("read error" ))
337+ var out bytes.Buffer
338+
339+ s := NewY2BService (WithIO (errReader , & out )).(* svc )
340+
341+ _ , err := s .getCodeFromPrompt ("http://example.com/auth" , "http://localhost:8216" )
342+ if err == nil {
343+ t .Fatalf ("expected error from getCodeFromPrompt, got nil" )
344+ }
345+ if ! strings .Contains (err .Error (), "failed to read prompt" ) {
346+ t .Errorf ("expected error to contain %q, got %q" , "failed to read prompt" , err .Error ())
347+ }
348+ }
0 commit comments