package netbox import ( "context" "errors" "sync" "testing" "time" "git.unkin.net/unkin/bootapi/internal/model" ) // countingResolver records how many times the underlying resolver is hit. type countingResolver struct { mu sync.Mutex calls int host *model.Host err error } func (c *countingResolver) HostByMAC(context.Context, string) (*model.Host, error) { c.mu.Lock() defer c.mu.Unlock() c.calls++ return c.host, c.err } func (c *countingResolver) HostByName(context.Context, string) (*model.Host, error) { return c.HostByMAC(context.Background(), "") } func TestCacheHitAndExpiry(t *testing.T) { inner := &countingResolver{host: &model.Host{Hostname: "web01"}} cache := NewCache(inner, time.Minute) now := time.Unix(1000, 0) cache.now = func() time.Time { return now } // First call misses and resolves. if _, err := cache.HostByMAC(context.Background(), "aa:bb:cc:00:11:22"); err != nil { t.Fatal(err) } // Second call within TTL is a hit; the inner resolver is not called again. if _, err := cache.HostByMAC(context.Background(), "AA:BB:CC:00:11:22"); err != nil { t.Fatal(err) } if inner.calls != 1 { t.Fatalf("inner calls = %d, want 1 (second served from cache)", inner.calls) } if cache.Hits() != 1 || cache.Misses() != 1 { t.Fatalf("hits=%d misses=%d, want 1/1", cache.Hits(), cache.Misses()) } // Advance past the TTL -> next call misses and re-resolves. now = now.Add(2 * time.Minute) if _, err := cache.HostByMAC(context.Background(), "aa:bb:cc:00:11:22"); err != nil { t.Fatal(err) } if inner.calls != 2 { t.Fatalf("inner calls = %d, want 2 after expiry", inner.calls) } } func TestCacheDisabled(t *testing.T) { inner := &countingResolver{host: &model.Host{Hostname: "web01"}} cache := NewCache(inner, 0) // ttl <= 0 disables caching for i := 0; i < 3; i++ { if _, err := cache.HostByMAC(context.Background(), "aa:bb:cc:00:11:22"); err != nil { t.Fatal(err) } } if inner.calls != 3 { t.Fatalf("inner calls = %d, want 3 (cache disabled)", inner.calls) } } func TestCacheDoesNotCacheErrors(t *testing.T) { inner := &countingResolver{err: ErrNotFound} cache := NewCache(inner, time.Minute) for i := 0; i < 2; i++ { if _, err := cache.HostByMAC(context.Background(), "de:ad:be:ef:00:00"); !errors.Is(err, ErrNotFound) { t.Fatalf("err = %v, want ErrNotFound", err) } } if inner.calls != 2 { t.Fatalf("inner calls = %d, want 2 (errors are not cached)", inner.calls) } }