package library import ( "context" "errors" "math" "testing" "time" ) func TestValidateFingerprintSettings(t *testing.T) { with := func(edit func(*FingerprintSettings)) FingerprintSettings { s := DefaultFingerprintSettings edit(&s) return s } valid := map[string]FingerprintSettings{ "defaults": DefaultFingerprintSettings, "shortest length": with(func(s *FingerprintSettings) { s.ChromaprintLengthSec = minChromaprintLengthSec }), "longest length": with(func(s *FingerprintSettings) { s.ChromaprintLengthSec = maxChromaprintLengthSec }), "strictest match": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = minAcousticMaxBitErrorRate }), "loosest match": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = maxAcousticMaxBitErrorRate }), "fewest at once": with(func(s *FingerprintSettings) { s.BackfillConcurrency = minBackfillConcurrency }), "most at once": with(func(s *FingerprintSettings) { s.BackfillConcurrency = maxBackfillConcurrency }), "shortest interval": with(func(s *FingerprintSettings) { s.SweepIntervalHours = minSweepIntervalHours }), "longest interval": with(func(s *FingerprintSettings) { s.SweepIntervalHours = maxSweepIntervalHours }), "switched off": with(func(s *FingerprintSettings) { s.Enabled = false }), } for name, s := range valid { if err := validateFingerprintSettings(s); err != nil { t.Errorf("%s: rejected: %v", name, err) } } invalid := map[string]FingerprintSettings{ "length too short": with(func(s *FingerprintSettings) { s.ChromaprintLengthSec = minChromaprintLengthSec - 1 }), "length too long": with(func(s *FingerprintSettings) { s.ChromaprintLengthSec = maxChromaprintLengthSec + 1 }), "match too strict": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = minAcousticMaxBitErrorRate - 0.001 }), "match too loose": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = maxAcousticMaxBitErrorRate + 0.001 }), "match not a number": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = math.NaN() }), "none at once": with(func(s *FingerprintSettings) { s.BackfillConcurrency = 0 }), "too many at once": with(func(s *FingerprintSettings) { s.BackfillConcurrency = maxBackfillConcurrency + 1 }), "no interval": with(func(s *FingerprintSettings) { s.SweepIntervalHours = 0 }), "interval too long": with(func(s *FingerprintSettings) { s.SweepIntervalHours = maxSweepIntervalHours + 1 }), } for name, s := range invalid { if err := validateFingerprintSettings(s); !errors.Is(err, ErrFingerprintSettingOutOfRange) { t.Errorf("%s: err = %v, want ErrFingerprintSettingOutOfRange", name, err) } } } func TestFingerprintSettingsService_NilServesDefaults(t *testing.T) { var s *FingerprintSettingsService if got := s.Get(); got != DefaultFingerprintSettings { t.Fatalf("nil service Get = %+v, want the defaults", got) } // Validation still runs first, so a bad value is named rather than hidden // behind the missing service. bad := DefaultFingerprintSettings bad.BackfillConcurrency = 0 if _, err := s.Set(context.Background(), bad); !errors.Is(err, ErrFingerprintSettingOutOfRange) { t.Fatalf("nil service Set of a bad value: err = %v, want ErrFingerprintSettingOutOfRange", err) } } func TestFingerprintSettingsService_Integration(t *testing.T) { pool := newPool(t) ctx := context.Background() reload := func(step string) FingerprintSettings { t.Helper() fresh, err := NewFingerprintSettingsService(ctx, pool) if err != nil { t.Fatalf("%s: load: %v", step, err) } return fresh.Get() } withoutTime := func(s FingerprintSettings) FingerprintSettings { s.UpdatedAt = time.Time{} return s } svc, err := NewFingerprintSettingsService(ctx, pool) if err != nil { t.Fatalf("load: %v", err) } // ResetDB puts every column back to its migration default, so this pins the // Go defaults to migration 0061's. Were they to drift, a database that could // not be read would fingerprint differently from one that could. loaded := svc.Get() if loaded.UpdatedAt.IsZero() { t.Fatal("loaded settings carry no updated_at") } if withoutTime(loaded) != DefaultFingerprintSettings { t.Fatalf("stored defaults = %+v, want the Go defaults %+v", withoutTime(loaded), DefaultFingerprintSettings) } // Every bound the service accepts, the table accepts too. A CHECK tighter // than validate would turn a value the card allows into a 500. lowest := FingerprintSettings{ Enabled: false, ChromaprintLengthSec: minChromaprintLengthSec, AcousticMaxBitErrorRate: minAcousticMaxBitErrorRate, BackfillConcurrency: minBackfillConcurrency, SweepIntervalHours: minSweepIntervalHours, } highest := FingerprintSettings{ Enabled: true, ChromaprintLengthSec: maxChromaprintLengthSec, AcousticMaxBitErrorRate: maxAcousticMaxBitErrorRate, BackfillConcurrency: maxBackfillConcurrency, SweepIntervalHours: maxSweepIntervalHours, } for _, step := range []struct { name string want FingerprintSettings }{{"lowest", lowest}, {"highest", highest}} { saved, err := svc.Set(ctx, step.want) if err != nil { t.Fatalf("%s: save: %v", step.name, err) } // A save must move updated_at forward: it is what makes a sweep due. if !saved.UpdatedAt.After(loaded.UpdatedAt) { t.Fatalf("%s: updated_at %v did not move past %v", step.name, saved.UpdatedAt, loaded.UpdatedAt) } if withoutTime(saved) != step.want || withoutTime(svc.Get()) != step.want { t.Fatalf("%s: saved %+v, cached %+v, want %+v", step.name, saved, svc.Get(), step.want) } if got := withoutTime(reload(step.name)); got != step.want { t.Fatalf("%s: table holds %+v, want %+v", step.name, got, step.want) } } // An out-of-range save changes nothing, in the cache or the table. before := svc.Get() bad := highest bad.ChromaprintLengthSec = maxChromaprintLengthSec + 1 if _, err := svc.Set(ctx, bad); !errors.Is(err, ErrFingerprintSettingOutOfRange) { t.Fatalf("out-of-range save: err = %v, want ErrFingerprintSettingOutOfRange", err) } if svc.Get() != before { t.Fatalf("out-of-range save changed the cache to %+v", svc.Get()) } if got := reload("after out-of-range save"); got != before { t.Fatalf("out-of-range save changed the table to %+v", got) } }