180 lines
4.5 KiB
Go
180 lines
4.5 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
//go:build linux || freebsd
|
||
|
|
|
||
|
|
package storage
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bufio"
|
||
|
|
"encoding/json"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestNewsletterStore_AppendCreatesFile(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
path := filepath.Join(dir, "subs", "subscribers.jsonl")
|
||
|
|
s := NewNewsletterStore(path)
|
||
|
|
|
||
|
|
if err := s.Append(Subscriber{Email: "a@example.com", Form: "newsletter"}); err != nil {
|
||
|
|
t.Fatalf("Append: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := os.Stat(path); err != nil {
|
||
|
|
t.Fatalf("expected file to exist, got %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewsletterStore_AppendAndCount(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
path := filepath.Join(dir, "subs.jsonl")
|
||
|
|
s := NewNewsletterStore(path)
|
||
|
|
|
||
|
|
for i, e := range []string{"a@x.com", "b@x.com", "c@x.com"} {
|
||
|
|
if err := s.Append(Subscriber{Email: e, Form: "newsletter"}); err != nil {
|
||
|
|
t.Fatalf("Append #%d: %v", i, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
n, err := s.Count()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Count: %v", err)
|
||
|
|
}
|
||
|
|
if n != 3 {
|
||
|
|
t.Errorf("expected 3 subscribers, got %d", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewsletterStore_AppendIsOneLinePerRecord(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
path := filepath.Join(dir, "subs.jsonl")
|
||
|
|
s := NewNewsletterStore(path)
|
||
|
|
|
||
|
|
for _, e := range []string{"a@x.com", "b@x.com"} {
|
||
|
|
if err := s.Append(Subscriber{Email: e, Form: "n"}); err != nil {
|
||
|
|
t.Fatalf("Append: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Each line should be a self-contained JSON object.
|
||
|
|
data, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ReadFile: %v", err)
|
||
|
|
}
|
||
|
|
scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
||
|
|
count := 0
|
||
|
|
for scanner.Scan() {
|
||
|
|
line := scanner.Bytes()
|
||
|
|
if len(line) == 0 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
var sub Subscriber
|
||
|
|
if err := json.Unmarshal(line, &sub); err != nil {
|
||
|
|
t.Fatalf("Unmarshal line: %v", err)
|
||
|
|
}
|
||
|
|
count++
|
||
|
|
}
|
||
|
|
if err := scanner.Err(); err != nil {
|
||
|
|
t.Fatalf("scanner.Err: %v", err)
|
||
|
|
}
|
||
|
|
if count != 2 {
|
||
|
|
t.Errorf("expected 2 records, decoded %d", count)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewsletterStore_CountOnMissingFileIsZero(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
path := filepath.Join(dir, "does-not-exist.jsonl")
|
||
|
|
s := NewNewsletterStore(path)
|
||
|
|
|
||
|
|
n, err := s.Count()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Count on missing file: %v", err)
|
||
|
|
}
|
||
|
|
if n != 0 {
|
||
|
|
t.Errorf("expected 0 on missing file, got %d", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewsletterStore_ListReturnsInsertionOrder(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
path := filepath.Join(dir, "subs.jsonl")
|
||
|
|
s := NewNewsletterStore(path)
|
||
|
|
|
||
|
|
want := []string{"a@x.com", "b@x.com", "c@x.com"}
|
||
|
|
for _, e := range want {
|
||
|
|
_ = s.Append(Subscriber{Email: e, Form: "n"})
|
||
|
|
}
|
||
|
|
|
||
|
|
subs, err := s.List()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("List: %v", err)
|
||
|
|
}
|
||
|
|
if len(subs) != len(want) {
|
||
|
|
t.Fatalf("expected %d, got %d", len(want), len(subs))
|
||
|
|
}
|
||
|
|
for i, sub := range subs {
|
||
|
|
if sub.Email != want[i] {
|
||
|
|
t.Errorf("position %d: want %q, got %q", i, want[i], sub.Email)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewsletterStore_SkipsMalformedLines(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
path := filepath.Join(dir, "subs.jsonl")
|
||
|
|
|
||
|
|
// Write a mix of valid and garbage lines.
|
||
|
|
content := `{"email":"good1@x.com","form":"n","created_at":"2026-01-01T00:00:00Z"}
|
||
|
|
this is not valid json
|
||
|
|
{"email":"good2@x.com","form":"n","created_at":"2026-01-02T00:00:00Z"}
|
||
|
|
{not even close to json
|
||
|
|
`
|
||
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||
|
|
t.Fatalf("WriteFile: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
s := NewNewsletterStore(path)
|
||
|
|
n, err := s.Count()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Count: %v", err)
|
||
|
|
}
|
||
|
|
if n != 2 {
|
||
|
|
t.Errorf("expected 2 valid records (skipping malformed), got %d", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewsletterStore_PathReturnsConfiguredPath(t *testing.T) {
|
||
|
|
path := filepath.Join(t.TempDir(), "subs.jsonl")
|
||
|
|
s := NewNewsletterStore(path)
|
||
|
|
if got := s.Path(); got != path {
|
||
|
|
t.Errorf("Path: want %q, got %q", path, got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The dedupe wrapper records delivered addresses and reports repeats
|
||
|
|
// case-insensitively; underlying Append semantics stay untouched.
|
||
|
|
func TestDedupeNewsletterStore(t *testing.T) {
|
||
|
|
path := filepath.Join(t.TempDir(), "subs.jsonl")
|
||
|
|
store := NewNewsletterStore(path)
|
||
|
|
dedupe := NewDedupeNewsletterStore(store)
|
||
|
|
|
||
|
|
if dedupe.Has("Jane@Example.com") {
|
||
|
|
t.Fatal("fresh log should not contain the address")
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := dedupe.Append(Subscriber{Email: "jane@example.com"}); err != nil {
|
||
|
|
t.Fatalf("append: %v", err)
|
||
|
|
}
|
||
|
|
if !dedupe.Has(" Jane@Example.COM ") {
|
||
|
|
t.Error("address should be found after append, ignoring case and spaces")
|
||
|
|
}
|
||
|
|
n, err := store.Count()
|
||
|
|
if err != nil || n != 1 {
|
||
|
|
t.Fatalf("log lines = %d (err %v), want exactly one record", n, err)
|
||
|
|
}
|
||
|
|
}
|