Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions pkg/hoverclient/browser_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,16 +306,31 @@ func (b *browserBackend) probeExistingSession(ctx context.Context, c *Client) (b
req.Header.Set("User-Agent", c.UserAgent)
resp, err := c.do(req)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false, err
}
if ctxErr := ctx.Err(); ctxErr != nil {
return false, ctxErr
Comment thread
intel352 marked this conversation as resolved.
}
return false, nil
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
if ctxErr := ctx.Err(); ctxErr != nil {
return false, ctxErr
}
return false, nil
}
var body struct {
Succeeded bool `json:"succeeded"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false, err
}
if ctxErr := ctx.Err(); ctxErr != nil {
return false, ctxErr
}
return false, nil
}
return body.Succeeded, nil
Expand Down
111 changes: 92 additions & 19 deletions pkg/hoverclient/browser_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"net/url"
"os"
"strings"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -477,10 +478,10 @@ func TestBrowserBackend_LoginSkipsWhenFresh(t *testing.T) {
opts := newBrowserTestOpts(t)

// Count how many times the signin page is hit.
signinHits := 0
var signinHits atomic.Int32
mux := http.NewServeMux()
mux.HandleFunc("/signin", func(w http.ResponseWriter, r *http.Request) {
signinHits++
signinHits.Add(1)
http.SetCookie(w, &http.Cookie{Name: "__uzma", Value: "fake", Path: "/"})
_, _ = w.Write([]byte(`<html><body>signin</body></html>`))
})
Expand All @@ -502,26 +503,26 @@ func TestBrowserBackend_LoginSkipsWhenFresh(t *testing.T) {
if err := c.Login(ctx); err != nil {
t.Fatalf("first Login: %v", err)
}
firstHits := signinHits
firstHits := signinHits.Load()

// Second login: session is fresh — should not re-navigate.
if err := c.Login(ctx); err != nil {
t.Fatalf("second Login: %v", err)
}

// Signin page must not have been hit again.
if signinHits != firstHits {
t.Errorf("browser re-launched on second Login; signin page hit count went from %d to %d", firstHits, signinHits)
if got := signinHits.Load(); got != firstHits {
t.Errorf("browser re-launched on second Login; signin page hit count went from %d to %d", firstHits, got)
}
}

func TestBrowserBackend_LoginReusesWarmBrowserProfileSession(t *testing.T) {
opts := newBrowserTestOpts(t)

var signinHits, authHits, domainsHits int
var signinHits, authHits, domainsHits atomic.Int32
mux := http.NewServeMux()
mux.HandleFunc("/api/domains", func(w http.ResponseWriter, r *http.Request) {
domainsHits++
domainsHits.Add(1)
if _, err := r.Cookie("__uzma"); err != nil {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"succeeded":false,"error_code":"login"}`))
Expand All @@ -531,12 +532,12 @@ func TestBrowserBackend_LoginReusesWarmBrowserProfileSession(t *testing.T) {
_ = json.NewEncoder(w).Encode(map[string]any{"succeeded": true, "domains": []map[string]any{}})
})
mux.HandleFunc("/signin", func(w http.ResponseWriter, r *http.Request) {
signinHits++
signinHits.Add(1)
http.SetCookie(w, &http.Cookie{Name: "__uzma", Value: "fresh-from-signin", Path: "/"})
_, _ = w.Write([]byte(`<html><body>signin</body></html>`))
})
mux.HandleFunc("/signin/auth.json", func(w http.ResponseWriter, r *http.Request) {
authHits++
authHits.Add(1)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"succeeded": true, "status": "completed"})
})
Expand All @@ -555,10 +556,10 @@ func TestBrowserBackend_LoginReusesWarmBrowserProfileSession(t *testing.T) {
if err := c.Login(ctx); err != nil {
t.Fatalf("Login with warm profile: %v", err)
}
if signinHits != 0 || authHits != 0 {
t.Fatalf("warm profile should skip credential login; signinHits=%d authHits=%d", signinHits, authHits)
if gotSignin, gotAuth := signinHits.Load(), authHits.Load(); gotSignin != 0 || gotAuth != 0 {
t.Fatalf("warm profile should skip credential login; signinHits=%d authHits=%d", gotSignin, gotAuth)
}
if domainsHits == 0 {
if domainsHits.Load() == 0 {
t.Fatal("warm profile login did not probe /api/domains")
}
c.mu.Lock()
Expand All @@ -569,23 +570,71 @@ func TestBrowserBackend_LoginReusesWarmBrowserProfileSession(t *testing.T) {
}
}

func TestBrowserBackend_ProbeExistingSessionReturnsContextCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()

bb := newBrowserBackend(BrowserOptions{})
bb.overrideHost = "http://127.0.0.1:1"
c := &Client{
http: &http.Client{},
UserAgent: defaultUserAgent,
}

ok, err := bb.probeExistingSession(ctx, c)
if ok {
t.Fatal("probeExistingSession unexpectedly reused a canceled session")
}
if !errors.Is(err, context.Canceled) {
t.Fatalf("probeExistingSession error = %v, want context.Canceled", err)
}
}

func TestBrowserBackend_ProbeExistingSessionReturnsContextCancellationFromBodyRead(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())

bb := newBrowserBackend(BrowserOptions{})
bb.overrideHost = "https://example.test"
c := &Client{
http: &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: &cancelAfterPartialJSONBody{cancel: cancel},
Header: make(http.Header),
Request: req,
}, nil
}),
},
UserAgent: defaultUserAgent,
}

ok, err := bb.probeExistingSession(ctx, c)
if ok {
t.Fatal("probeExistingSession unexpectedly reused a canceled session")
}
if !errors.Is(err, context.Canceled) {
t.Fatalf("probeExistingSession error = %v, want context.Canceled", err)
}
}

func TestBrowserBackend_LoginFallsBackWhenWarmProfileUnauthenticated(t *testing.T) {
opts := newBrowserTestOpts(t)

var signinHits, authHits, domainsHits int
var signinHits, authHits, domainsHits atomic.Int32
mux := http.NewServeMux()
mux.HandleFunc("/api/domains", func(w http.ResponseWriter, r *http.Request) {
domainsHits++
domainsHits.Add(1)
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"succeeded":false,"error_code":"login"}`))
})
mux.HandleFunc("/signin", func(w http.ResponseWriter, r *http.Request) {
signinHits++
signinHits.Add(1)
http.SetCookie(w, &http.Cookie{Name: "__uzma", Value: "fresh-from-signin", Path: "/"})
_, _ = w.Write([]byte(`<html><body>signin</body></html>`))
})
mux.HandleFunc("/signin/auth.json", func(w http.ResponseWriter, r *http.Request) {
authHits++
authHits.Add(1)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"succeeded": true, "status": "completed"})
})
Expand All @@ -603,12 +652,36 @@ func TestBrowserBackend_LoginFallsBackWhenWarmProfileUnauthenticated(t *testing.
if err := c.Login(ctx); err != nil {
t.Fatalf("Login after stale warm profile probe: %v", err)
}
if domainsHits == 0 {
if domainsHits.Load() == 0 {
t.Fatal("stale profile login did not probe /api/domains before fallback")
}
if signinHits == 0 || authHits == 0 {
t.Fatalf("stale profile should fall back to credential login; signinHits=%d authHits=%d", signinHits, authHits)
if gotSignin, gotAuth := signinHits.Load(), authHits.Load(); gotSignin == 0 || gotAuth == 0 {
t.Fatalf("stale profile should fall back to credential login; signinHits=%d authHits=%d", gotSignin, gotAuth)
}
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}

type cancelAfterPartialJSONBody struct {
cancel context.CancelFunc
sent bool
}

func (b *cancelAfterPartialJSONBody) Read(p []byte) (int, error) {
if !b.sent {
b.sent = true
return copy(p, `{"succeeded":`), nil
}
b.cancel()
return 0, context.Canceled
}

func (b *cancelAfterPartialJSONBody) Close() error {
return nil
}

func seedBrowserProfileCookie(t *testing.T, opts BrowserOptions, baseURL, name, value string) {
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "workflow-plugin-hover",
"version": "0.5.6",
"version": "0.5.7",
"description": "Hover DNS provider (browser-style login + TOTP, no official SDK)",
"author": "GoCodeAlone",
"license": "MIT",
Expand Down