package selfip import ( "context" "encoding/json" "errors" "net/netip" "os" "path/filepath" "testing" "time" "git.wntrmute.dev/kyle/sift/internal/guard" ) func TestAThenBBothDeniedUntilKeepExpires(t *testing.T) { g := &guard.Guard{} now := time.Date(2026, 9, 22, 12, 0, 0, 0, time.UTC) dir := t.TempDir() a := netip.MustParseAddr("8.8.8.8") b := netip.MustParseAddr("1.1.1.1") cur := a d := &Discoverer{ Cfg: Config{ Sources: []string{"ddns:wan.example"}, KeepPrevious: 24 * time.Hour, Path: filepath.Join(dir, "self_ip.json"), StartupWait: time.Second, }, Guard: g, Lookup: func(ctx context.Context, host string) ([]netip.Addr, error) { return []netip.Addr{cur}, nil }, } d.now = func() time.Time { return now } if err := d.RunOnce(context.Background()); err != nil { t.Fatal(err) } if err := g.CheckAddr(a); err == nil { t.Fatal("A should be denied") } if err := g.CheckAddr(b); err != nil { t.Fatalf("B should still pass: %v", err) } now = now.Add(time.Minute) cur = b if err := d.RunOnce(context.Background()); err != nil { t.Fatal(err) } if err := g.CheckAddr(a); err == nil { t.Fatal("A should still be denied during keep_previous") } if err := g.CheckAddr(b); err == nil { t.Fatal("B should be denied") } now = now.Add(25 * time.Hour) if err := d.RunOnce(context.Background()); err != nil { t.Fatal(err) } if err := g.CheckAddr(a); err != nil { t.Fatalf("A should have aged out: %v", err) } if err := g.CheckAddr(b); err == nil { t.Fatal("B should still be denied") } } func TestNonGlobalIgnored(t *testing.T) { g := &guard.Guard{} d := &Discoverer{ Cfg: Config{Sources: []string{"ddns:wan.example"}, KeepPrevious: time.Hour}, Guard: g, Lookup: func(ctx context.Context, host string) ([]netip.Addr, error) { return []netip.Addr{ netip.MustParseAddr("10.0.0.1"), netip.MustParseAddr("127.0.0.1"), netip.MustParseAddr("169.254.1.1"), }, nil }, } d.now = time.Now if err := d.RunOnce(context.Background()); err == nil { t.Fatal("want failure when every answer is non-global") } if p := g.SelfDeny(); len(p) != 0 { t.Fatalf("self deny = %v, want empty", p) } } func TestFailureKeepsPersisted(t *testing.T) { g := &guard.Guard{} dir := t.TempDir() path := filepath.Join(dir, "self_ip.json") now := time.Date(2026, 9, 22, 12, 0, 0, 0, time.UTC) persist := file{Entries: []Entry{{ Addr: "9.9.9.9", Source: "ddns:wan.example", Seen: now, }}} b, _ := json.Marshal(persist) if err := os.WriteFile(path, b, 0o640); err != nil { t.Fatal(err) } d := &Discoverer{ Cfg: Config{ Sources: []string{"ddns:wan.example"}, KeepPrevious: 24 * time.Hour, Path: path, }, Guard: g, Lookup: func(ctx context.Context, host string) ([]netip.Addr, error) { return nil, errors.New("nxdomain") }, } d.now = func() time.Time { return now } if err := d.Load(); err != nil { t.Fatal(err) } if err := g.CheckAddr(netip.MustParseAddr("9.9.9.9")); err == nil { t.Fatal("persisted addr should be denied after load") } if err := d.RunOnce(context.Background()); err == nil { t.Fatal("want discovery error") } if d.Failures.Load() == 0 { t.Fatal("want a failure count") } if err := g.CheckAddr(netip.MustParseAddr("9.9.9.9")); err == nil { t.Fatal("persisted addr must survive a failed refresh") } } func TestParseTrace(t *testing.T) { a, err := parseTrace("fl=x\nip=1.1.1.1\nts=1\n") if err != nil { t.Fatal(err) } if a.String() != "1.1.1.1" { t.Fatalf("got %s", a) } if _, err := parseTrace("nope"); err == nil { t.Fatal("want error") } } func TestTraceGetter(t *testing.T) { g := &guard.Guard{} d := &Discoverer{ Cfg: Config{Sources: []string{"trace:https://1.1.1.1/cdn-cgi/trace"}, KeepPrevious: time.Hour}, Guard: g, Get: func(ctx context.Context, rawURL string) ([]byte, error) { if rawURL != "https://1.1.1.1/cdn-cgi/trace" { t.Fatalf("url %s", rawURL) } return []byte("ip=8.8.4.4\n"), nil }, } d.now = time.Now if err := d.RunOnce(context.Background()); err != nil { t.Fatal(err) } if err := g.CheckAddr(netip.MustParseAddr("8.8.4.4")); err == nil { t.Fatal("want denied") } }