1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package proxy
import (
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/armanlife/firefox-vpn-client/core/internal/fxa"
"github.com/armanlife/firefox-vpn-client/core/internal/guardian"
)
func TestProxyControllerSleepOrDoneWakesOnClose(t *testing.T) {
t.Parallel()
controller := &Controller{done: make(chan struct{})}
done := make(chan bool, 1)
go func() {
done <- controller.sleepOrDone(time.Hour)
}()
if err := controller.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
select {
case keptGoing := <-done:
if keptGoing {
t.Fatal("expected sleepOrDone to stop after Close")
}
case <-time.After(time.Second):
t.Fatal("sleepOrDone did not wake after Close")
}
}
func TestProxyControllerRenewWithTimeoutTimesOut(t *testing.T) {
t.Parallel()
controller := &Controller{done: make(chan struct{})}
controller.renewMu.Lock()
defer controller.renewMu.Unlock()
timedOutCh := make(chan bool, 1)
go func() {
_, timedOut := controller.renewWithTimeout(20 * time.Millisecond)
timedOutCh <- timedOut
}()
select {
case timedOut := <-timedOutCh:
if !timedOut {
t.Fatal("expected renewWithTimeout to time out")
}
case <-time.After(time.Second):
t.Fatal("renewWithTimeout did not return")
}
}
func TestProxyControllerRenewWithTimeoutStopsWhenClosed(t *testing.T) {
t.Parallel()
controller := &Controller{done: make(chan struct{})}
close(controller.done)
_, timedOut := controller.renewWithTimeout(time.Hour)
if timedOut {
t.Fatal("expected closed controller not to report renewal timeout")
}
}
func TestRenewalRetryDelayBacksOffTerminalFailures(t *testing.T) {
now := time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC)
reset := now.Add(2 * time.Hour)
tests := []struct {
name string
err error
want time.Duration
code string
}{
{"transient", errors.New("connection reset"), proxyPassRetryDelay, "renewal_failed"},
{"reauth", fmt.Errorf("refresh: %w", fxa.ErrReauthRequired), terminalRenewalRetryDelay, "reauth_required"},
{"region", fmt.Errorf("x: %w", guardian.ErrRegionUnavailable), terminalRenewalRetryDelay, "region_unavailable"},
{"quota with reset", &passQuotaError{usage: &guardian.ProxyUsage{Limit: 1, Reset: reset}}, 2*time.Hour + time.Minute, "quota_exhausted"},
}
for _, tc := range tests {
if got := RenewalRetryDelay(tc.err, now); got != tc.want {
t.Errorf("%s: delay %s, want %s", tc.name, got, tc.want)
}
if got := renewalFailureCode(tc.err); got != tc.code {
t.Errorf("%s: code %q, want %q", tc.name, got, tc.code)
}
}
}
func TestProxyControllerRebuildCooldownAvoidsGuardianStorm(t *testing.T) {
t.Parallel()
controller := &Controller{
current: &managedSession{session: &fakeProxySession{}, expiresAt: time.Now().Add(10 * time.Minute)},
}
failed := controller.current
controller.passBackoff.record(time.Now(), errors.New("CONNECT 401"), errors.New("previous rebuild failed"))
err := controller.rebuildSession(failed, &proxyConnectHTTPError{statusCode: 401, status: "401 Unauthorized"})
if !errors.Is(err, errRebuildCoolingDown) || !strings.Contains(err.Error(), "previous rebuild failed") {
t.Fatalf("expected a cooldown error without contacting Guardian, got %v", err)
}
}
func TestProxyControllerNextRenewalDelayHasFloorAfterRenewal(t *testing.T) {
t.Parallel()
controller := &Controller{
current: &managedSession{expiresAt: time.Now().Add(time.Minute)},
lastRenewalAt: time.Now(),
}
if got := controller.nextRenewalDelay(); got < minRenewalInterval-time.Second {
t.Fatalf("expected a renewal floor near %s, got %s", minRenewalInterval, got)
}
}
// A replaced session is reported, so the daemon can test the connection;
// a failed or skipped rebuild is not.
func TestSessionRebuildIsReported(t *testing.T) {
t.Parallel()
rebuilt := make(chan struct{}, 2)
controller := &Controller{
current: &managedSession{session: &fakeProxySession{}, expiresAt: time.Now().Add(10 * time.Minute)},
refreshSession: func() error { return nil },
OnSessionRebuilt: func() { rebuilt <- struct{}{} },
}
if err := controller.rebuildSession(controller.current, errProxySessionUnhealthy); err != nil {
t.Fatal(err)
}
select {
case <-rebuilt:
case <-time.After(2 * time.Second):
t.Fatal("the rebuild was not reported")
}
controller.refreshSession = func() error { return errors.New("no pass") }
if err := controller.rebuildSession(controller.current, errProxySessionUnhealthy); err == nil {
t.Fatal("expected the failure")
}
select {
case <-rebuilt:
t.Fatal("a failed rebuild must not be reported")
case <-time.After(50 * time.Millisecond):
}
}