Files
2026-04-09 06:42:21 -07:00

340 lines
8.0 KiB
Go

package autofillcache
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"git.julianfamily.org/keepassgo/internal/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",
Fields: map[string]string{
"AndroidApp1": "androidapp://com.lights.mobile",
"KP2A_URL_1": "https://surveillance.crew.example.invalid/account",
},
},
},
}, 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 surveillance.crew.example.invalid", got.Entries[1].Host)
}
if len(got.Entries[1].Targets) != 3 {
t.Fatalf("len(second targets) = %d, want 3", len(got.Entries[1].Targets))
}
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 TestResolveReportsFoundAmbiguousAndMissingStatuses(t *testing.T) {
t.Parallel()
cache := File{
Entries: []Entry{
{
ID: "one",
Title: "Admin Login",
Username: "admin",
Password: "secret1",
URL: "https://example.com/admin",
Host: "example.com",
},
{
ID: "two",
Title: "Shared Login A",
Username: "shared-a",
Password: "secret2",
URL: "https://shared.example.com",
Host: "shared.example.com",
},
{
ID: "three",
Title: "Shared Login B",
Username: "shared-b",
Password: "secret3",
URL: "https://shared.example.com",
Host: "shared.example.com",
},
},
}
if got := Resolve(cache, "https://example.com/admin/login"); got.Status != MatchStatusFound || got.Entry.ID != "one" {
t.Fatalf("Resolve(found) = %#v, want found entry one", got)
}
if got := Resolve(cache, "https://shared.example.com"); got.Status != MatchStatusAmbiguous {
t.Fatalf("Resolve(ambiguous) = %#v, want ambiguous", got)
}
if got := Resolve(cache, "https://nowhere.invalid"); got.Status != MatchStatusMissing {
t.Fatalf("Resolve(missing) = %#v, want missing", got)
}
}
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)
}
}
func TestMatchSupportsAndroidAppPackageTargets(t *testing.T) {
t.Parallel()
cache := File{
Entries: []Entry{
{
ID: "one",
Title: "Thunderbird",
Username: "mail-user",
Password: "secret1",
URL: "androidapp://org.mozilla.thunderbird/login",
Host: "org.mozilla.thunderbird",
},
},
}
got, ok := Match(cache, "androidapp://org.mozilla.thunderbird")
if !ok {
t.Fatalf("Match() found no entry")
}
if got.ID != "one" {
t.Fatalf("Match() entry = %q, want one", got.ID)
}
}
func TestMatchRejectsAmbiguousAndroidAppPackageTargets(t *testing.T) {
t.Parallel()
cache := File{
Entries: []Entry{
{
ID: "one",
Title: "Thunderbird Primary",
Username: "mail-user",
Password: "secret1",
URL: "androidapp://org.mozilla.thunderbird",
Host: "org.mozilla.thunderbird",
},
{
ID: "two",
Title: "Thunderbird Secondary",
Username: "other-user",
Password: "secret2",
URL: "androidapp://org.mozilla.thunderbird",
Host: "org.mozilla.thunderbird",
},
},
}
if _, ok := Match(cache, "androidapp://org.mozilla.thunderbird"); ok {
t.Fatalf("Match() unexpectedly resolved ambiguous android app package target")
}
}
func TestMatchUsesAndroidAppCustomFieldTarget(t *testing.T) {
t.Parallel()
cache := File{
Entries: []Entry{
{
ID: "one",
Title: "Blink",
Username: "blink-user",
Password: "secret1",
URL: "https://account.blinknetwork.com",
Host: "account.blinknetwork.com",
Targets: []string{"https://account.blinknetwork.com", "androidapp://com.blinknetwork.mobile2"},
},
},
}
got, ok := Match(cache, "androidapp://com.blinknetwork.mobile2")
if !ok {
t.Fatalf("Match() found no entry")
}
if got.ID != "one" {
t.Fatalf("Match() entry = %q, want one", got.ID)
}
}
func TestMatchUsesKP2AURLCustomFieldTarget(t *testing.T) {
t.Parallel()
cache := File{
Entries: []Entry{
{
ID: "one",
Title: "Blink",
Username: "blink-user",
Password: "secret1",
URL: "https://blinknetwork.com",
Host: "blinknetwork.com",
Targets: []string{"https://blinknetwork.com", "https://account.blinknetwork.com"},
},
},
}
got, ok := Match(cache, "https://account.blinknetwork.com")
if !ok {
t.Fatalf("Match() found no entry")
}
if got.ID != "one" {
t.Fatalf("Match() entry = %q, want one", got.ID)
}
}