Skip to content

Commit 5db7eb5

Browse files
grokifyclaude
andcommitted
test(publicclient): cover unlock failures, locked logout and closed bodies
Refs: RMI-SYSTEMFORGE-186 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2e3296c commit 5db7eb5

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

‎identity/publicclient/publicclient_test.go‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
"testing"
1616
"time"
1717

18+
"github.com/grokify/goauth/dpop"
19+
1820
"github.com/plexusone/systemforge/identity/publicclient"
1921
"github.com/plexusone/systemforge/identity/relyingparty"
2022
"github.com/plexusone/systemforge/identity/systemauth"
@@ -426,3 +428,106 @@ func TestFileStore(t *testing.T) {
426428
t.Errorf("deleting nothing: %v", err)
427429
}
428430
}
431+
432+
// lockingStore is a FileStore whose lock counts its holders and can fail
433+
// to release.
434+
type lockingStore struct {
435+
publicclient.FileStore
436+
locks int
437+
unlockErr error
438+
}
439+
440+
func (l *lockingStore) Lock(ctx context.Context) (func() error, error) {
441+
unlock, err := l.FileStore.Lock(ctx)
442+
if err != nil {
443+
return nil, err
444+
}
445+
l.locks++
446+
return func() error { return errors.Join(unlock(), l.unlockErr) }, nil
447+
}
448+
449+
// TestStoreLock checks that a refresh reports a failure to release the
450+
// store's lock, and that logout holds the lock.
451+
func TestStoreLock(t *testing.T) {
452+
e := newEnv(t)
453+
ctx := t.Context()
454+
if _, err := publicclient.Login(ctx, e.config(), e.browser(t)); err != nil {
455+
t.Fatalf("login: %v", err)
456+
}
457+
ls := &lockingStore{FileStore: e.store, unlockErr: errors.New("unlock failed")}
458+
cfg := e.config()
459+
cfg.Store = ls
460+
461+
e.expire(t)
462+
s, err := publicclient.Open(ctx, cfg)
463+
if err != nil {
464+
t.Fatal(err)
465+
}
466+
if _, err := s.AccessToken(ctx); err == nil || !strings.Contains(err.Error(), "unlock failed") {
467+
t.Errorf("refresh with a failing unlock: %v", err)
468+
}
469+
470+
ls.unlockErr, ls.locks = nil, 0
471+
if err := s.Logout(ctx); err != nil {
472+
t.Fatalf("logout: %v", err)
473+
}
474+
if ls.locks != 1 {
475+
t.Errorf("logout took the store's lock %d times, want 1", ls.locks)
476+
}
477+
}
478+
479+
// trackedBody records whether it was closed.
480+
type trackedBody struct {
481+
io.Reader
482+
closed bool
483+
}
484+
485+
func (b *trackedBody) Close() error {
486+
b.closed = true
487+
return nil
488+
}
489+
490+
// TestTransportClosesBody checks the RoundTripper contract: a request that
491+
// cannot be sent, here because the access token expired and there is no
492+
// refresh token, has its body closed.
493+
func TestTransportClosesBody(t *testing.T) {
494+
ctx := t.Context()
495+
key, err := dpop.GenerateKeyPair()
496+
if err != nil {
497+
t.Fatal(err)
498+
}
499+
serialized, err := key.SerializeJSON()
500+
if err != nil {
501+
t.Fatal(err)
502+
}
503+
store := publicclient.FileStore{Path: filepath.Join(t.TempDir(), "credentials.json")}
504+
if err := store.Save(ctx, &publicclient.Credentials{
505+
Issuer: "https://auth.example.com", ClientID: "cli", DPoPKey: serialized,
506+
AccessToken: "expired", Expiry: time.Now().Add(-time.Hour),
507+
}); err != nil {
508+
t.Fatal(err)
509+
}
510+
s, err := publicclient.Open(ctx, publicclient.Config{
511+
Issuer: "https://auth.example.com", ClientID: "cli", Store: store,
512+
Metadata: &relyingparty.Metadata{ //nolint:gosec // G101: endpoint URLs, not credentials
513+
Issuer: "https://auth.example.com",
514+
AuthorizationEndpoint: "https://auth.example.com/oauth/authorize",
515+
TokenEndpoint: "https://auth.example.com/oauth/token",
516+
JWKSURI: "https://auth.example.com/.well-known/jwks.json",
517+
},
518+
})
519+
if err != nil {
520+
t.Fatal(err)
521+
}
522+
body := &trackedBody{Reader: strings.NewReader("payload")}
523+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.example.com/x", body)
524+
if err != nil {
525+
t.Fatal(err)
526+
}
527+
if _, err := s.Transport(nil).RoundTrip(req); !errors.Is(err, publicclient.ErrNotLoggedIn) {
528+
t.Errorf("round trip without a usable token: %v", err)
529+
}
530+
if !body.closed {
531+
t.Error("the request body was not closed")
532+
}
533+
}

0 commit comments

Comments
 (0)