package skiprules import ( "bytes" "log/slog" "os" "path/filepath" "strings" "testing" "time" ) const ( buyerA = `[{"comment":"a","active":true,"rules":[{"rule_type":"buyer","values":["A"]}]}]` buyerB = `[{"comment":"b","active":true,"rules":[{"rule_type":"buyer","values":["B"]}]}]` ) // write writes contents to path with a modification time of when, so each // write is seen as a change however fast the test runs. func write(t *testing.T, path, contents string, when time.Time) { t.Helper() if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatal(err) } if err := os.Chtimes(path, when, when); err != nil { t.Fatal(err) } } // comment returns the comment of the filter rules match o with, or "". func comment(rules *Rules, buyer string) string { matched, _ := rules.Match(opportunity(buyer, "x")) return matched } func TestOpenRefuses(t *testing.T) { dir := t.TempDir() if _, err := Open(filepath.Join(dir, "missing.json")); err == nil { t.Error("a missing file was accepted") } path := filepath.Join(dir, "bad.json") write(t, path, `[{}]`, time.Now()) _, err := Open(path) if err == nil || !strings.Contains(err.Error(), "bad.json: filter 1") { t.Errorf("err = %v; want one naming the file and filter", err) } } // Rules reads the file again when it changes, keeps the last good rules when // it is invalid, and has none while it is missing, logging each problem once. func TestFileReloads(t *testing.T) { path := filepath.Join(t.TempDir(), "rules.json") start := time.Now().Add(-time.Hour) write(t, path, buyerA, start) var logged bytes.Buffer logger := slog.New(slog.NewTextHandler(&logged, nil)) file, err := Open(path, WithLogger(logger)) if err != nil { t.Fatal(err) } if got := comment(file.Rules(), "A"); got != "a" { t.Fatalf("first rules: %q; want a", got) } write(t, path, buyerB, start.Add(time.Minute)) if got := comment(file.Rules(), "B"); got != "b" { t.Errorf("after an edit: %q; want b", got) } if !strings.Contains(logged.String(), "skip rules reloaded") { t.Errorf("reload not logged:\n%s", logged.String()) } // Invalid: the last good rules are kept, and the error logged once. logged.Reset() write(t, path, `[{]`, start.Add(2*time.Minute)) file.Rules() if got := comment(file.Rules(), "B"); got != "b" { t.Errorf("after a bad edit: %q; want b kept", got) } if strings.Count(logged.String(), "skip rules file invalid") != 1 { t.Errorf("want the bad file logged once:\n%s", logged.String()) } // Missing: no rules, and a warning logged once. logged.Reset() os.Remove(path) file.Rules() if rules := file.Rules(); rules != nil { t.Errorf("missing file: rules %v; want nil", rules) } if strings.Count(logged.String(), "skip rules file missing") != 1 { t.Errorf("want the missing file logged once:\n%s", logged.String()) } // Back again, even with an old modification time. write(t, path, buyerA, start) if got := comment(file.Rules(), "A"); got != "a" { t.Errorf("file back: %q; want a", got) } } // A change that keeps the modification time but changes the size is still // seen. func TestFileReloadsOnSize(t *testing.T) { path := filepath.Join(t.TempDir(), "rules.json") when := time.Now().Add(-time.Hour) write(t, path, buyerA, when) file, err := Open(path, WithLogger(slog.New(slog.DiscardHandler))) if err != nil { t.Fatal(err) } write(t, path, buyerB+"\n", when) if got := comment(file.Rules(), "B"); got != "b" { t.Errorf("after a same-time edit: %q; want b", got) } }