2026-07-26 20:46:50 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
2026-06-20 20:48:09 +02:00
|
|
|
//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)
|
|
|
|
|
}
|
|
|
|
|
}
|