platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
80
selective-vpn-api/app/refresh_coordinator_test.go
Normal file
80
selective-vpn-api/app/refresh_coordinator_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRefreshCoordinatorLifecycle(t *testing.T) {
|
||||
now := time.Date(2026, time.March, 9, 21, 0, 0, 0, time.UTC)
|
||||
rc := newRefreshCoordinator(10*time.Minute, 2*time.Second, 60*time.Second)
|
||||
|
||||
if !rc.shouldRefresh(now, false, false) {
|
||||
t.Fatalf("expected refresh when cache is empty")
|
||||
}
|
||||
if !rc.beginRefresh(now, false, false) {
|
||||
t.Fatalf("expected beginRefresh=true for empty cache")
|
||||
}
|
||||
if rc.shouldRefresh(now, false, false) {
|
||||
t.Fatalf("must not refresh while refresh is in progress")
|
||||
}
|
||||
|
||||
rc.finishSuccess(now)
|
||||
snap := rc.snapshot(now)
|
||||
if snap.RefreshInProgress {
|
||||
t.Fatalf("refresh must be finished after success")
|
||||
}
|
||||
if snap.Stale {
|
||||
t.Fatalf("fresh snapshot must not be stale")
|
||||
}
|
||||
if snap.LastError != "" || snap.NextRetryAt != "" {
|
||||
t.Fatalf("success must clear error and retry metadata: %#v", snap)
|
||||
}
|
||||
|
||||
if rc.shouldRefresh(now.Add(5*time.Minute), false, true) {
|
||||
t.Fatalf("fresh cache should not refresh yet")
|
||||
}
|
||||
if !rc.shouldRefresh(now.Add(11*time.Minute), false, true) {
|
||||
t.Fatalf("stale cache should refresh")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshCoordinatorBackoffAndReset(t *testing.T) {
|
||||
start := time.Date(2026, time.March, 9, 21, 5, 0, 0, time.UTC)
|
||||
rc := newRefreshCoordinator(10*time.Minute, 2*time.Second, 60*time.Second)
|
||||
|
||||
type step struct {
|
||||
at time.Time
|
||||
expected time.Duration
|
||||
}
|
||||
steps := []step{
|
||||
{at: start, expected: 2 * time.Second},
|
||||
{at: start.Add(2 * time.Second), expected: 4 * time.Second},
|
||||
{at: start.Add(6 * time.Second), expected: 8 * time.Second},
|
||||
{at: start.Add(14 * time.Second), expected: 16 * time.Second},
|
||||
{at: start.Add(30 * time.Second), expected: 32 * time.Second},
|
||||
{at: start.Add(62 * time.Second), expected: 60 * time.Second},
|
||||
}
|
||||
|
||||
for i, st := range steps {
|
||||
rc.finishError("probe failed", st.at)
|
||||
got := rc.nextRetryAt().Sub(st.at)
|
||||
if got != st.expected {
|
||||
t.Fatalf("step=%d unexpected backoff: got=%s want=%s", i+1, got, st.expected)
|
||||
}
|
||||
if rc.lastError() == "" {
|
||||
t.Fatalf("step=%d expected non-empty lastError", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
rc.finishSuccess(start.Add(2 * time.Minute))
|
||||
if rc.consecutiveErrors() != 0 {
|
||||
t.Fatalf("expected consecutiveErrors reset, got %d", rc.consecutiveErrors())
|
||||
}
|
||||
if !rc.nextRetryAt().IsZero() {
|
||||
t.Fatalf("expected nextRetryAt reset")
|
||||
}
|
||||
if rc.lastError() != "" {
|
||||
t.Fatalf("expected lastError reset, got %q", rc.lastError())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user