package autofillcache import ( "encoding/json" "os" "path/filepath" "testing" "time" "git.julianfamily.org/keepassgo/vault" ) func TestBuildFiltersAndNormalizesEntries(t *testing.T) { t.Parallel() now := time.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC) got := Build(vault.Model{ Entries: []vault.Entry{ { ID: "one", Title: "Chrome Test", Username: "joe", Password: "secret", URL: "https://10.0.2.2:8443/login", Path: []string{"Crew", "Internet"}, }, { ID: "two", Title: "No Password", Username: "joe", URL: "https://example.com", }, { ID: "three", Title: "Bare Host", Username: "user", Password: "pass", URL: "surveillance.crew.example.invalid", }, }, }, now) if len(got.Entries) != 2 { t.Fatalf("entry count = %d, want 2", len(got.Entries)) } if got.Entries[0].Host != "10.0.2.2" { t.Fatalf("first host = %q, want 10.0.2.2", got.Entries[0].Host) } if got.Entries[1].Host != "surveillance.crew.example.invalid" { t.Fatalf("second host = %q, want lights.julianfamily.org", got.Entries[1].Host) } if got.UpdatedAt != "2026-03-31T12:00:00Z" { t.Fatalf("updatedAt = %q", got.UpdatedAt) } } func TestWriteAndClear(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "autofill-cache.json") model := vault.Model{ Entries: []vault.Entry{ {ID: "one", Title: "Chrome Test", Username: "joe", Password: "secret", URL: "https://10.0.2.2:8443/login"}, }, } if err := Write(path, model, time.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC)); err != nil { t.Fatalf("Write() error = %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile() error = %v", err) } var got File if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if len(got.Entries) != 1 || got.Entries[0].Host != "10.0.2.2" { t.Fatalf("cache entries = %#v", got.Entries) } if err := Clear(path); err != nil { t.Fatalf("Clear() error = %v", err) } if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("cache path still exists, stat err = %v", err) } } func TestMatchChoosesExactURLWhenHostsRepeat(t *testing.T) { t.Parallel() cache := File{ Entries: []Entry{ { ID: "one", Title: "Primary Login", Username: "first", Password: "secret1", URL: "https://10.0.2.2:8443/login/", Host: "10.0.2.2", }, { ID: "two", Title: "Alt Login", Username: "second", Password: "secret2", URL: "https://10.0.2.2:8443/alt/", Host: "10.0.2.2", }, }, } got, ok := Match(cache, "https://10.0.2.2:8443/alt/") if !ok { t.Fatalf("Match() found no entry") } if got.ID != "two" { t.Fatalf("Match() entry = %q, want two", got.ID) } } func TestMatchRejectsAmbiguousSharedHost(t *testing.T) { t.Parallel() cache := File{ Entries: []Entry{ { ID: "one", Title: "Host A", Username: "first", Password: "secret1", URL: "https://surveillance.crew.example.invalid/", Host: "surveillance.crew.example.invalid", }, { ID: "two", Title: "Host B", Username: "second", Password: "secret2", URL: "https://surveillance.crew.example.invalid/", Host: "surveillance.crew.example.invalid", }, }, } if _, ok := Match(cache, "https://surveillance.crew.example.invalid/"); ok { t.Fatalf("Match() unexpectedly resolved ambiguous shared host") } } func TestMatchChoosesLongestPathPrefix(t *testing.T) { t.Parallel() cache := File{ Entries: []Entry{ { ID: "one", Title: "Generic Login", Username: "generic", Password: "secret1", URL: "https://example.com/", Host: "example.com", }, { ID: "two", Title: "Admin Login", Username: "admin", Password: "secret2", URL: "https://example.com/admin", Host: "example.com", }, }, } got, ok := Match(cache, "https://example.com/admin/login") if !ok { t.Fatalf("Match() found no entry") } if got.ID != "two" { t.Fatalf("Match() entry = %q, want two", got.ID) } }