package main import ( "bytes" "net/netip" "os" "strings" "testing" "time" ) func TestParseExampleLines(t *testing.T) { data, err := os.ReadFile("loglines_example.txt") if err != nil { t.Fatal(err) } p := newParser(time.Time{}) if err := p.ingest(bytes.NewReader(data)); err != nil { t.Fatal(err) } want := map[string]int{ "41.93.28.11": 2, "45.191.4.252": 2, "164.77.161.180": 2, "212.128.3.152": 2, } if len(p.events) != len(want) { t.Fatalf("got %d events, want %d", len(p.events), len(want)) } for ipStr, count := range want { ev, ok := p.events[netip.MustParseAddr(ipStr)] if !ok { t.Errorf("missing event for %s", ipStr) continue } if ev.count != count { t.Errorf("%s: count %d, want %d", ipStr, ev.count, count) } } var out bytes.Buffer if err := writeCSV(&out, p.sorted()); err != nil { t.Fatal(err) } lines := strings.Split(strings.TrimSpace(out.String()), "\n") if lines[0] != "IP,Categories,Comment,ReportDate" { t.Errorf("bad header: %q", lines[0]) } if len(lines) != 5 { t.Fatalf("got %d csv lines, want 5:\n%s", len(lines), out.String()) } if !strings.Contains(lines[1], "41.93.28.11,\"18,22\",") || !strings.HasSuffix(lines[1], "2026-10-06T12:04:24Z") { t.Errorf("unexpected first row: %q", lines[1]) } // The descriptive "Invalid user ... from" line should win over the // later "Connection closed by invalid user" line for the comment. if !strings.Contains(lines[2], ",Invalid user production from 45.191.4.252 port 38924 (2 events),") { t.Errorf("unexpected comment in row: %q", lines[2]) } // AllowUsers rejections are reported with their own message. if !strings.Contains(lines[1], "User root from 41.93.28.11 not allowed because not listed in AllowUsers (2 events)") { t.Errorf("unexpected comment in row: %q", lines[1]) } } func TestExclusions(t *testing.T) { p := newParser(time.Time{}) for _, line := range []string{ "2026-10-06T12:04:24.687263829Z Invalid user git from 1.2.3.4 port 38924", "2026-10-06T12:04:24.687263829Z User git from 1.2.3.5 not allowed because not listed in AllowUsers", "2026-10-06T12:04:24.687263829Z Invalid user bob from 172.19.0.1 port 38924", "2026-10-06T12:04:24.687263829Z Invalid user bob from 10.0.0.9 port 38924", "2026-10-06T12:04:24.687263829Z Invalid user bob from 127.0.0.1 port 38924", "2026-10-06T12:04:24.687263829Z Invalid user bob from fd00::1 port 38924", "2026-10-06T12:04:24.687263829Z Invalid user bob from not-an-ip port 38924", "2026-10-06T12:04:24.687263829Z Received disconnect from 8.8.8.8 port 53534:11: disconnected by user", "garbage line without timestamp", } { p.handleLine(line) } if len(p.events) != 0 { t.Errorf("expected no events, got %v", p.events) } if p.skipped != 1 { t.Errorf("skipped = %d, want 1", p.skipped) } } func TestSinceCutoff(t *testing.T) { cutoff := time.Date(2026, 10, 6, 12, 4, 29, 0, time.UTC) p := newParser(cutoff) p.handleLine("2026-10-06T12:04:24.687263829Z Invalid user production from 45.191.4.252 port 38924") p.handleLine("2026-10-06T12:04:30.222573149Z Invalid user production from 212.128.3.152 port 60626") if len(p.events) != 1 { t.Fatalf("got %d events, want 1", len(p.events)) } if _, ok := p.events[netip.MustParseAddr("212.128.3.152")]; !ok { t.Error("expected only the post-cutoff event to remain") } } func TestIPv6AndLatestWins(t *testing.T) { p := newParser(time.Time{}) p.handleLine("2026-10-06T12:04:24Z Invalid user a from 2001:db8::1 port 1") p.handleLine("2026-10-06T12:04:26Z Invalid user b from 2001:db8::1 port 2") p.handleLine("2026-10-06T12:04:25Z Invalid user c from 2001:db8::1 port 3") ev := p.events[netip.MustParseAddr("2001:db8::1")] if ev == nil || ev.count != 3 || ev.user != "b" { t.Fatalf("unexpected event: %+v", ev) } var out bytes.Buffer if err := writeCSV(&out, p.sorted()); err != nil { t.Fatal(err) } if !strings.Contains(out.String(), "(3 events)") { t.Errorf("expected event count in comment:\n%s", out.String()) } }