164 lines
3.8 KiB
Go
164 lines
3.8 KiB
Go
package dedupe
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/ejr/feedkit/event"
|
|
"gitea.maximumdirect.net/ejr/feedkit/processors"
|
|
)
|
|
|
|
func TestNewProcessorValidation(t *testing.T) {
|
|
t.Run("rejects non-positive maxEntries", func(t *testing.T) {
|
|
for _, maxEntries := range []int{0, -1} {
|
|
p, err := NewProcessor(maxEntries)
|
|
if err == nil {
|
|
t.Fatalf("expected error for maxEntries=%d, got nil", maxEntries)
|
|
}
|
|
if p != nil {
|
|
t.Fatalf("expected nil processor for maxEntries=%d", maxEntries)
|
|
}
|
|
if !strings.Contains(err.Error(), "maxEntries") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("accepts positive maxEntries", func(t *testing.T) {
|
|
p, err := NewProcessor(1)
|
|
if err != nil {
|
|
t.Fatalf("NewProcessor error: %v", err)
|
|
}
|
|
if p == nil {
|
|
t.Fatalf("expected processor, got nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestProcessorFirstSeenAndDuplicate(t *testing.T) {
|
|
p, err := NewProcessor(8)
|
|
if err != nil {
|
|
t.Fatalf("NewProcessor error: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
first := testEvent("evt-1")
|
|
|
|
out, err := p.Process(ctx, first)
|
|
if err != nil {
|
|
t.Fatalf("Process first error: %v", err)
|
|
}
|
|
if out == nil {
|
|
t.Fatalf("expected first event to pass through")
|
|
}
|
|
if out.ID != first.ID {
|
|
t.Fatalf("expected unchanged ID %q, got %q", first.ID, out.ID)
|
|
}
|
|
|
|
out, err = p.Process(ctx, first)
|
|
if err != nil {
|
|
t.Fatalf("Process duplicate error: %v", err)
|
|
}
|
|
if out != nil {
|
|
t.Fatalf("expected duplicate to be dropped, got %#v", out)
|
|
}
|
|
|
|
out, err = p.Process(ctx, testEvent("evt-2"))
|
|
if err != nil {
|
|
t.Fatalf("Process second unique error: %v", err)
|
|
}
|
|
if out == nil {
|
|
t.Fatalf("expected second unique event to pass through")
|
|
}
|
|
}
|
|
|
|
func TestProcessorLRUEvictionAndPromotion(t *testing.T) {
|
|
p, err := NewProcessor(2)
|
|
if err != nil {
|
|
t.Fatalf("NewProcessor error: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
mustPass(t, p, ctx, "a")
|
|
mustPass(t, p, ctx, "b")
|
|
mustDrop(t, p, ctx, "a") // promote "a" so "b" becomes least-recently-used
|
|
mustPass(t, p, ctx, "c") // evicts "b"
|
|
mustDrop(t, p, ctx, "a") // "a" should still be tracked after promotion
|
|
mustPass(t, p, ctx, "b") // "b" was evicted, so now it passes again
|
|
}
|
|
|
|
func TestProcessorRejectsBlankID(t *testing.T) {
|
|
p, err := NewProcessor(4)
|
|
if err != nil {
|
|
t.Fatalf("NewProcessor error: %v", err)
|
|
}
|
|
|
|
in := testEvent(" ")
|
|
out, err := p.Process(context.Background(), in)
|
|
if err == nil {
|
|
t.Fatalf("expected error for blank ID")
|
|
}
|
|
if out != nil {
|
|
t.Fatalf("expected nil output on error, got %#v", out)
|
|
}
|
|
if !strings.Contains(err.Error(), "event ID is required") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFactoryWithRegistry(t *testing.T) {
|
|
r := processors.NewRegistry()
|
|
r.Register("dedupe", Factory(3))
|
|
|
|
p, err := r.Build("dedupe")
|
|
if err != nil {
|
|
t.Fatalf("Build error: %v", err)
|
|
}
|
|
if p == nil {
|
|
t.Fatalf("expected processor, got nil")
|
|
}
|
|
|
|
out, err := p.Process(context.Background(), testEvent("evt-factory-1"))
|
|
if err != nil {
|
|
t.Fatalf("Process error: %v", err)
|
|
}
|
|
if out == nil {
|
|
t.Fatalf("expected first event to pass through")
|
|
}
|
|
}
|
|
|
|
func mustPass(t *testing.T, p *Processor, ctx context.Context, id string) {
|
|
t.Helper()
|
|
out, err := p.Process(ctx, testEvent(id))
|
|
if err != nil {
|
|
t.Fatalf("expected pass for id=%q, got error: %v", id, err)
|
|
}
|
|
if out == nil {
|
|
t.Fatalf("expected pass for id=%q, got drop", id)
|
|
}
|
|
}
|
|
|
|
func mustDrop(t *testing.T, p *Processor, ctx context.Context, id string) {
|
|
t.Helper()
|
|
out, err := p.Process(ctx, testEvent(id))
|
|
if err != nil {
|
|
t.Fatalf("expected drop for id=%q, got error: %v", id, err)
|
|
}
|
|
if out != nil {
|
|
t.Fatalf("expected drop for id=%q, got output", id)
|
|
}
|
|
}
|
|
|
|
func testEvent(id string) event.Event {
|
|
return event.Event{
|
|
ID: id,
|
|
Kind: event.Kind("observation"),
|
|
Source: "source-1",
|
|
EmittedAt: time.Now().UTC(),
|
|
Payload: map[string]any{"ok": true},
|
|
}
|
|
}
|