92 lines
2.5 KiB
Go
92 lines
2.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 (
|
||
|
|
"encoding/json"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestPendingStoreRoundTrip(t *testing.T) {
|
||
|
|
p := NewPendingStore(filepath.Join(t.TempDir(), "pending.json"), 72*time.Hour)
|
||
|
|
|
||
|
|
if _, ok := p.Consume("unknown-token"); ok {
|
||
|
|
t.Fatal("unknown token must not consume")
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := p.Issue("token-a", PendingSubscription{Email: "Jane@Example.COM", IP: "203.0.113.9"}); err != nil {
|
||
|
|
t.Fatalf("issue: %v", err)
|
||
|
|
}
|
||
|
|
sub, ok := p.Consume("token-a")
|
||
|
|
if !ok {
|
||
|
|
t.Fatal("valid token must consume")
|
||
|
|
}
|
||
|
|
if sub.Email != "Jane@Example.COM" || sub.IP != "203.0.113.9" {
|
||
|
|
t.Errorf("consumed entry = %+v", sub)
|
||
|
|
}
|
||
|
|
if _, ok := p.Consume("token-a"); ok {
|
||
|
|
t.Error("token must be single-use")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// An expired entry is never returned and is purged from the file.
|
||
|
|
func TestPendingStoreExpiry(t *testing.T) {
|
||
|
|
path := filepath.Join(t.TempDir(), "pending.json")
|
||
|
|
stale, _ := json.Marshal(pendingFile{Schema: pendingSchema, Entries: map[string]PendingSubscription{
|
||
|
|
hashToken("old"): {Email: "a@b.c", CreatedAt: time.Now().Add(-96 * time.Hour)},
|
||
|
|
}})
|
||
|
|
if err := os.WriteFile(path, stale, 0o600); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, ok := NewPendingStore(path, 72*time.Hour).Consume("old"); ok {
|
||
|
|
t.Fatal("expired token must not consume")
|
||
|
|
}
|
||
|
|
raw, _ := os.ReadFile(path)
|
||
|
|
if strings.Contains(string(raw), `"a@b.c"`) {
|
||
|
|
t.Error("expired entry was not purged from the file")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Issue replaces the previous live token for the same address.
|
||
|
|
func TestPendingStoreRotation(t *testing.T) {
|
||
|
|
path := filepath.Join(t.TempDir(), "pending.json")
|
||
|
|
p := NewPendingStore(path, 72*time.Hour)
|
||
|
|
if err := p.Issue("first", PendingSubscription{Email: "jane@example.com"}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := p.Issue("second", PendingSubscription{Email: "jane@example.com"}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
raw, _ := os.ReadFile(path)
|
||
|
|
if strings.Count(string(raw), "@") != 1 {
|
||
|
|
t.Errorf("expected a single live entry after rotation, got %s", raw)
|
||
|
|
}
|
||
|
|
if _, ok := p.Consume("first"); ok {
|
||
|
|
t.Error("superseded token must no longer work")
|
||
|
|
}
|
||
|
|
if _, ok := p.Consume("second"); !ok {
|
||
|
|
t.Error("current token must still work")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRandomTokenIsHexAndUnique(t *testing.T) {
|
||
|
|
a, err := RandomToken()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
b, err := RandomToken()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if len(a) != 64 || a == b {
|
||
|
|
t.Errorf("tokens = %q %q, want unique 64-char hex", a, b)
|
||
|
|
}
|
||
|
|
}
|