mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 21:13:00 +08:00
226 lines
7.9 KiB
Go
226 lines
7.9 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/policy"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/presets"
|
|
)
|
|
|
|
func refreshRequest(iface, key string) Request {
|
|
return Request{Kind: KindPresetsRefresh, Interface: iface, IdempotencyKey: key}
|
|
}
|
|
|
|
func TestPresetRefreshSerializesCacheAcrossLinesAndCancellation(t *testing.T) {
|
|
h := newHarness(t, func(o *Options) { o.Lines = func(r Request) string { return r.Interface } })
|
|
first, err := h.Submit(t.Context(), refreshRequest("wan", "first"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.awaitStart()
|
|
h.lingerOn(first.ActionID)
|
|
second, err := h.Submit(t.Context(), refreshRequest("wan2", "second"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.expectNoStart("two refreshes must not write the same cache")
|
|
h.cancel(first.ActionID)
|
|
h.awaitCancelSeen(first.ActionID)
|
|
h.expectNoStart("cancelled worker has not released the cache")
|
|
h.succeed(first.ActionID)
|
|
if next := h.awaitStart(); next.ID != second.ActionID {
|
|
t.Fatalf("started %+v", next)
|
|
}
|
|
h.succeed(second.ActionID)
|
|
h.awaitState(second.ActionID, StateSucceeded)
|
|
if h.state(first.ActionID).State != StateCancelled {
|
|
t.Fatal("late success reopened cancellation")
|
|
}
|
|
}
|
|
|
|
func TestPresetRefreshYieldsToAuthenticationAndKeysIncludeLine(t *testing.T) {
|
|
h := newHarness(t, func(o *Options) { o.Parallel = 1 })
|
|
blocker := h.submit(KindLogin, "a", "block")
|
|
h.awaitStart()
|
|
request := refreshRequest("wan", "refresh")
|
|
refresh, err := h.Submit(t.Context(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, err := h.Submit(t.Context(), request)
|
|
if err != nil || again.ActionID != refresh.ActionID || !again.Duplicate {
|
|
t.Fatalf("duplicate: %+v %v", again, err)
|
|
}
|
|
request.Interface = "wan2"
|
|
if code, _ := domain.CodeOf(h.submitExpectingError(request)); code != domain.CodeConflict {
|
|
t.Fatal(code)
|
|
}
|
|
manual := h.submit(KindLogin, "b", "manual")
|
|
h.succeed(blocker.ActionID)
|
|
if next := h.awaitStart(); next.ID != manual.ActionID {
|
|
t.Fatal("refresh outranked manual action")
|
|
}
|
|
h.succeed(manual.ActionID)
|
|
if next := h.awaitStart(); next.ID != refresh.ActionID {
|
|
t.Fatal("refresh did not follow manual action")
|
|
}
|
|
h.succeed(refresh.ActionID)
|
|
if KindPresetsRefresh.Priority() != policy.PriorityPeriodic || KindPresetsRefresh.Manual() {
|
|
t.Fatal("refresh priority")
|
|
}
|
|
}
|
|
|
|
type presetFetchFunc func(context.Context, string) ([]byte, error)
|
|
|
|
func (f presetFetchFunc) Fetch(ctx context.Context, s string) ([]byte, error) { return f(ctx, s) }
|
|
|
|
const workerCatalogue = `{"schema_version":1,"updated_at":"2026-09-16","schools":[{"short_name":"new","name":"New","status":"active"}]}`
|
|
|
|
func TestPresetWorkerBindingBudgetsFallbackAndClose(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "cache.json")
|
|
cache := presets.NewCache(path)
|
|
closed, opened, fetched := 0, 0, 0
|
|
worker := PresetRefresher{Cache: cache, Sources: []string{"bad", "good"}, Now: func() time.Time { return epoch }}
|
|
worker.Resolve = func(ctx context.Context, iface string, seq uint64) (domain.Binding, error) {
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok || time.Until(deadline) > PresetRefreshBudget || time.Until(deadline) < 30*time.Second {
|
|
t.Fatal("missing total budget")
|
|
}
|
|
if iface != "wan2" || seq != 42 {
|
|
t.Fatalf("wrong selected line %q/%d", iface, seq)
|
|
}
|
|
return domain.Binding{LogicalIface: iface, Generation: seq, L3Device: "eth1", SourceIPv4: netip.MustParseAddr("192.0.2.2")}, nil
|
|
}
|
|
worker.Open = func(b domain.Binding) (presets.Fetcher, func(), error) {
|
|
opened++
|
|
if b.L3Device != "eth1" || b.SourceIPv4.String() != "192.0.2.2" {
|
|
t.Fatal("lost binding")
|
|
}
|
|
return presetFetchFunc(func(ctx context.Context, source string) ([]byte, error) {
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok || time.Until(deadline) > presets.FetchTimeout {
|
|
t.Fatal("missing per-source deadline")
|
|
}
|
|
fetched++
|
|
if source == "bad" {
|
|
return []byte("<html>captive portal</html>"), nil
|
|
}
|
|
return []byte(workerCatalogue), nil
|
|
}), func() { closed++ }, nil
|
|
}
|
|
action := Action{Request: refreshRequest("wan2", "key"), Sequence: 42}
|
|
var phase Phase
|
|
result := worker.Run(t.Context(), action, func(p Phase) { phase = p })
|
|
if result.State != StateSucceeded || closed != 1 || opened != 1 || fetched != 2 || phase != PhaseFetch {
|
|
t.Fatalf("outcome %+v close/open/fetch %d/%d/%d", result, closed, opened, fetched)
|
|
}
|
|
entry, exists, err := cache.Load()
|
|
if err != nil || !exists || string(entry.Raw) != workerCatalogue {
|
|
t.Fatalf("cache %+v %v", entry, err)
|
|
}
|
|
// A successful response older than the cache does not publish the older view.
|
|
worker.Sources = []string{"good"}
|
|
newer := []byte(`{"schema_version":1,"updated_at":"2099","schools":[]}`)
|
|
catalogue, _ := presets.Parse(newer)
|
|
if err := cache.Save(newer, catalogue, "existing", epoch); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result = worker.Run(t.Context(), action, func(Phase) {})
|
|
if result.State != StateSucceeded || result.Message != "已检查预设来源,保留较新的本地缓存" {
|
|
t.Fatal(result)
|
|
}
|
|
entry, _, _ = cache.Load()
|
|
if string(entry.Raw) != string(newer) {
|
|
t.Fatal("downgrade")
|
|
}
|
|
}
|
|
|
|
func TestPresetWorkerFailuresAndLateCancellationDoNotWrite(t *testing.T) {
|
|
for _, mode := range []string{"resolve", "wrong-line", "unready", "generation", "open", "sources", "cancel", "deadline", "early-cancel"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "cache.json")
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
opened, closed := 0, 0
|
|
worker := PresetRefresher{Cache: presets.NewCache(path), Sources: []string{"source"}}
|
|
worker.Resolve = func(context.Context, string, uint64) (domain.Binding, error) {
|
|
b := domain.Binding{LogicalIface: "wan", Generation: 1, L3Device: "eth0", SourceIPv4: netip.MustParseAddr("192.0.2.1")}
|
|
switch mode {
|
|
case "resolve":
|
|
return b, domain.Errorf(domain.CodeBindingUnavailable, "down")
|
|
case "wrong-line":
|
|
b.LogicalIface = "other"
|
|
case "unready":
|
|
b.L3Device = ""
|
|
case "generation":
|
|
b.Generation = 2
|
|
case "deadline":
|
|
return b, context.DeadlineExceeded
|
|
}
|
|
return b, nil
|
|
}
|
|
worker.Open = func(domain.Binding) (presets.Fetcher, func(), error) {
|
|
opened++
|
|
if mode == "open" {
|
|
return nil, nil, errors.New("cannot open")
|
|
}
|
|
return presetFetchFunc(func(context.Context, string) ([]byte, error) {
|
|
if mode == "sources" {
|
|
return nil, errors.New("offline")
|
|
}
|
|
cancel()
|
|
return []byte(workerCatalogue), nil
|
|
}), func() { closed++ }, nil
|
|
}
|
|
if mode == "early-cancel" {
|
|
cancel()
|
|
}
|
|
result := worker.Run(ctx, Action{Request: refreshRequest("wan", "key"), Sequence: 1}, func(Phase) {})
|
|
if result.State != StateFailed {
|
|
t.Fatal(result)
|
|
}
|
|
if mode == "cancel" && result.Code != domain.CodeCancelled {
|
|
t.Fatal(result)
|
|
}
|
|
if mode == "deadline" && result.Code != domain.CodeDeadlineExceeded {
|
|
t.Fatal(result)
|
|
}
|
|
if mode == "cancel" || mode == "sources" {
|
|
if opened != 1 || closed != 1 {
|
|
t.Fatal("client not closed")
|
|
}
|
|
} else if mode != "open" && opened != 0 {
|
|
t.Fatal("opened invalid binding")
|
|
}
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Fatal("failure wrote cache")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPresetRequestRejectsAmbiguousInputs(t *testing.T) {
|
|
for _, iface := range []string{"", "wan\x00x", "wan other", " wan", "/wan"} {
|
|
if err := refreshRequest(iface, "key").Validate(); err == nil {
|
|
t.Fatalf("accepted %q", iface)
|
|
}
|
|
}
|
|
for _, request := range []Request{
|
|
{Kind: KindPresetsRefresh, Interface: "wan", AccountID: "a", IdempotencyKey: "k"},
|
|
{Kind: KindPresetsRefresh, Interface: "wan", HotspotID: "h", IdempotencyKey: "k"},
|
|
{Kind: KindPresetsRefresh, Interface: "wan", IgnoreQuiet: true, IdempotencyKey: "k"},
|
|
{Kind: KindLogin, Interface: "wan", AccountID: "a", IdempotencyKey: "k"},
|
|
} {
|
|
if request.Validate() == nil {
|
|
t.Fatal("accepted ambiguous request")
|
|
}
|
|
}
|
|
}
|