- **Shortcuts**: Add hostname normalization with domain stripping and hyphen folding. Include alias generation for cached hosts. - **Shell Hook**: Automate 24h cache refresh trigger with shell startup hook. Add install/uninstall commands for bash, zsh, and fish. - **Wizard**: Extend setup wizard to configure shortcuts (domains, hyphen stripping) and default SSH port. - **Cache**: Add `GetByShortcut` for resolving hosts via normalized shortcuts. Implement `NeedsRefresh` / `SetRefreshed` logic for refresh timestamps. - **Tests**: Comprehensive unit tests for shortcuts, hook installation, cache refresh, and alias generation. - **Docs**: Update README with shortcuts, shell hook, and default SSH port configuration.
This commit is contained in:
Vendored
+158
@@ -300,6 +300,164 @@ func TestMarkUsed_RoundtripViaSave(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- GetByShortcut tests ---
|
||||
|
||||
func TestGetByShortcut_MatchFresh(t *testing.T) {
|
||||
c := New("", 3600)
|
||||
c.Upsert(Entry{Name: "web01.example.com", IP: "10.0.0.1", Kind: "device"})
|
||||
|
||||
normalize := func(s string) string {
|
||||
// strip .example.com
|
||||
if len(s) > len(".example.com") && s[len(s)-len(".example.com"):] == ".example.com" {
|
||||
s = s[:len(s)-len(".example.com")]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
e, found, fresh := c.GetByShortcut("web01", normalize)
|
||||
if !found {
|
||||
t.Fatal("expected entry to be found")
|
||||
}
|
||||
if !fresh {
|
||||
t.Error("entry just inserted should be fresh")
|
||||
}
|
||||
if e.IP != "10.0.0.1" {
|
||||
t.Errorf("IP: got %q, want %q", e.IP, "10.0.0.1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetByShortcut_MatchStale(t *testing.T) {
|
||||
c := New("", 1) // 1 second TTL
|
||||
stale := Entry{Name: "web01.example.com", IP: "10.0.0.1", Kind: "device", CachedAt: time.Now().Add(-2 * time.Second)}
|
||||
c.mu.Lock()
|
||||
c.entries["web01.example.com"] = stale
|
||||
c.mu.Unlock()
|
||||
|
||||
normalize := func(s string) string {
|
||||
if len(s) > len(".example.com") && s[len(s)-len(".example.com"):] == ".example.com" {
|
||||
s = s[:len(s)-len(".example.com")]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
_, found, fresh := c.GetByShortcut("web01", normalize)
|
||||
if !found {
|
||||
t.Fatal("expected entry to be found even when stale")
|
||||
}
|
||||
if fresh {
|
||||
t.Error("entry older than TTL should not be fresh")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetByShortcut_NotFound(t *testing.T) {
|
||||
c := New("", 3600)
|
||||
c.Upsert(Entry{Name: "db01.example.com", IP: "10.0.0.2", Kind: "device"})
|
||||
|
||||
_, found, _ := c.GetByShortcut("web01", func(s string) string { return s })
|
||||
if found {
|
||||
t.Error("should not find an entry that does not match the shortcut")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetByShortcut_EmptyCache(t *testing.T) {
|
||||
c := New("", 3600)
|
||||
_, found, _ := c.GetByShortcut("web01", func(s string) string { return s })
|
||||
if found {
|
||||
t.Error("empty cache should return found=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetByShortcut_MultiDomain(t *testing.T) {
|
||||
c := New("", 3600)
|
||||
// Entry uses second domain (.example.de)
|
||||
c.Upsert(Entry{Name: "web01.example.de", IP: "10.0.0.5", Kind: "vm"})
|
||||
|
||||
normalize := func(s string) string {
|
||||
for _, suffix := range []string{".example.com", ".example.de"} {
|
||||
if len(s) > len(suffix) && s[len(s)-len(suffix):] == suffix {
|
||||
return s[:len(s)-len(suffix)]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
e, found, _ := c.GetByShortcut("web01", normalize)
|
||||
if !found {
|
||||
t.Fatal("should match entry with second configured domain")
|
||||
}
|
||||
if e.IP != "10.0.0.5" {
|
||||
t.Errorf("IP: got %q, want %q", e.IP, "10.0.0.5")
|
||||
}
|
||||
}
|
||||
|
||||
// --- NeedsRefresh / SetRefreshed tests ---
|
||||
// These tests require NeedsRefresh(time.Duration) bool and SetRefreshed() error
|
||||
// to be implemented on *Cache. The refreshStamp field (path to the timestamp file)
|
||||
// must be set before calling these methods.
|
||||
|
||||
func TestNeedsRefresh_NeverRefreshed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
stampPath := filepath.Join(dir, "last_refresh")
|
||||
c := New(filepath.Join(dir, "cache.json"), 3600)
|
||||
c.refreshStamp = stampPath
|
||||
|
||||
if !c.NeedsRefresh(24 * time.Hour) {
|
||||
t.Error("NeedsRefresh should return true when last_refresh file does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedsRefresh_JustRefreshed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
stampPath := filepath.Join(dir, "last_refresh")
|
||||
c := New(filepath.Join(dir, "cache.json"), 3600)
|
||||
c.refreshStamp = stampPath
|
||||
|
||||
if err := c.SetRefreshed(); err != nil {
|
||||
t.Fatalf("SetRefreshed: %v", err)
|
||||
}
|
||||
if c.NeedsRefresh(24 * time.Hour) {
|
||||
t.Error("NeedsRefresh should return false immediately after SetRefreshed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedsRefresh_Stale(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
stampPath := filepath.Join(dir, "last_refresh")
|
||||
// Write a timestamp 25 hours ago
|
||||
oldTime := time.Now().Add(-25 * time.Hour).Format(time.RFC3339)
|
||||
if err := os.WriteFile(stampPath, []byte(oldTime), 0o644); err != nil {
|
||||
t.Fatalf("writing stamp: %v", err)
|
||||
}
|
||||
|
||||
c := New(filepath.Join(dir, "cache.json"), 3600)
|
||||
c.refreshStamp = stampPath
|
||||
|
||||
if !c.NeedsRefresh(24 * time.Hour) {
|
||||
t.Error("NeedsRefresh should return true when last_refresh is older than duration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRefreshed_Roundtrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
stampPath := filepath.Join(dir, "last_refresh")
|
||||
c := New(filepath.Join(dir, "cache.json"), 3600)
|
||||
c.refreshStamp = stampPath
|
||||
|
||||
if err := c.SetRefreshed(); err != nil {
|
||||
t.Fatalf("SetRefreshed: %v", err)
|
||||
}
|
||||
|
||||
// Just refreshed: 24h window → not stale
|
||||
if c.NeedsRefresh(24 * time.Hour) {
|
||||
t.Error("NeedsRefresh(24h) should be false right after SetRefreshed")
|
||||
}
|
||||
|
||||
// Zero duration: everything is stale
|
||||
if !c.NeedsRefresh(0) {
|
||||
t.Error("NeedsRefresh(0) should always return true")
|
||||
}
|
||||
}
|
||||
|
||||
// tempFile writes content to a temp file and returns its path.
|
||||
func tempFile(t *testing.T, content []byte) string {
|
||||
t.Helper()
|
||||
|
||||
Reference in New Issue
Block a user