package ddns import ( "context" "testing" "time" ) func TestNewRunner_ZeroStatus(t *testing.T) { rn := NewRunner() s := rn.GetStatus() if s.Enabled { t.Error("new runner should have Enabled=false") } if s.CurrentIP != "" { t.Errorf("new runner should have empty CurrentIP, got %q", s.CurrentIP) } if !s.LastChecked.IsZero() { t.Error("new runner should have zero LastChecked") } } func TestTrigger_NonBlocking(t *testing.T) { rn := NewRunner() done := make(chan struct{}) go func() { rn.Trigger() rn.Trigger() close(done) }() select { case <-done: case <-time.After(time.Second): t.Error("Trigger blocked unexpectedly") } } func staticParams(token string, records []string) ParamsFunc { return func() Params { return Params{APIToken: token, Records: records, IntervalMinutes: 5} } } func TestStart_SetsEnabledStatus(t *testing.T) { rn := NewRunner() started := make(chan struct{}, 1) rn.ipFetcher = func() (string, error) { select { case started <- struct{}{}: default: } return "1.2.3.4", nil } rn.Start(context.Background(), staticParams("tok", []string{"a.example.com"})) select { case <-started: case <-time.After(time.Second): t.Fatal("goroutine did not start in time") } if !rn.GetStatus().Enabled { t.Error("expected Enabled=true after Start with valid params") } rn.Stop() if rn.GetStatus().Enabled { t.Error("expected Enabled=false after Stop()") } } func TestStart_InvalidParams_SetsDisabled(t *testing.T) { for _, pfn := range []ParamsFunc{ func() Params { return Params{} }, func() Params { return Params{APIToken: ""} }, func() Params { return Params{APIToken: "tok", Records: nil} }, } { rn := NewRunner() rn.Start(context.Background(), pfn) if rn.GetStatus().Enabled { t.Errorf("expected Enabled=false for empty params") } } } func TestStart_Restart_KeepsEnabled(t *testing.T) { rn := NewRunner() reached := make(chan struct{}, 2) rn.ipFetcher = func() (string, error) { select { case reached <- struct{}{}: default: } return "1.2.3.4", nil } pfn := staticParams("tok", []string{"a.example.com"}) rn.Start(context.Background(), pfn) select { case <-reached: case <-time.After(time.Second): t.Fatal("first goroutine did not start") } // Restart — old goroutine is cancelled, new one starts rn.Start(context.Background(), pfn) if !rn.GetStatus().Enabled { t.Error("expected Enabled=true after restart") } rn.Stop() } func TestStop_ClearsEnabled(t *testing.T) { rn := NewRunner() rn.mu.Lock() rn.status.Enabled = true rn.mu.Unlock() rn.Stop() if rn.GetStatus().Enabled { t.Error("expected Enabled=false after Stop()") } } func TestStart_CancelsAndRestarts(t *testing.T) { rn := NewRunner() block := make(chan struct{}) firstReady := make(chan struct{}, 1) rn.ipFetcher = func() (string, error) { select { case firstReady <- struct{}{}: default: } <-block return "1.2.3.4", nil } pfn := staticParams("tok", []string{"a.example.com"}) rn.Start(context.Background(), pfn) select { case <-firstReady: case <-time.After(time.Second): t.Fatal("first goroutine did not start") } close(block) // Second Start with disabled params must not deadlock done := make(chan struct{}) go func() { rn.Start(context.Background(), func() Params { return Params{} }) close(done) }() select { case <-done: case <-time.After(time.Second): t.Error("second Start blocked unexpectedly") } } func TestCheckAndUpdate_UpdatesCurrentIP(t *testing.T) { rn := NewRunner() rn.ipFetcher = func() (string, error) { return "5.6.7.8", nil } rn.paramsFn = staticParams("fake", []string{"a.example.com"}) rn.checkAndUpdate() if rn.GetStatus().CurrentIP != "5.6.7.8" { t.Errorf("expected CurrentIP=5.6.7.8, got %q", rn.GetStatus().CurrentIP) } } func TestCheckAndUpdate_SkipsWhenNoParams(t *testing.T) { rn := NewRunner() rn.ipFetcher = func() (string, error) { return "1.2.3.4", nil } rn.paramsFn = func() Params { return Params{} } rn.checkAndUpdate() if rn.GetStatus().CurrentIP != "" { t.Error("expected empty CurrentIP when params have no token/records") } } func TestCheckAndUpdate_ParamsFuncCalledEachTime(t *testing.T) { rn := NewRunner() rn.ipFetcher = func() (string, error) { return "1.2.3.4", nil } calls := 0 rn.paramsFn = func() Params { calls++ return Params{APIToken: "fake", Records: []string{"a.example.com"}} } rn.checkAndUpdate() rn.checkAndUpdate() if calls != 2 { t.Errorf("expected paramsFn called 2 times, got %d", calls) } } func TestGetStatus_ReflectsManualUpdate(t *testing.T) { rn := NewRunner() rn.mu.Lock() rn.status.CurrentIP = "1.2.3.4" rn.status.LastChecked = time.Now() rn.status.Enabled = true rn.mu.Unlock() s := rn.GetStatus() if s.CurrentIP != "1.2.3.4" { t.Errorf("got CurrentIP %q, want 1.2.3.4", s.CurrentIP) } if !s.Enabled { t.Error("expected Enabled=true") } }