mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-11 17:28:05 +02:00
Security: - WebSocket events are routed to their owner only (no cross-user leak); hub close is idempotent (fixes double-close panic), adds ping/pong and write deadlines. - Session tokens stored as SHA-256 (migration 008 keeps sessions valid); single-query auth middleware puts the user in the request context. - Client IP only trusts X-Forwarded-For from TRUSTED_PROXIES; rate limiter map is bounded; per-user limit on AI summaries. - Argon2id at OWASP minimum with a concurrency cap; constant-time login for unknown emails; atomic first-admin bootstrap; REGISTRATION_ENABLED. - CSP/HSTS/COOP headers, same-origin guard on mutations, body size limits, wider SSRF denylist, bounded feed/page/AI response reads, generic errors. - Upgrade chi, pgx, x/net, x/text, x/crypto (known CVEs); commit go.sum. Performance: - List endpoints return a plain-text excerpt and reading time instead of full HTML; content is sanitized once at ingest (legacy rows backfilled). - Keyset pagination on (sort_at, id) with matching partial indexes; redundant indexes dropped (migration 007). - Fetcher: bounded worker pool, conditional GET (ETag/Last-Modified), exponential backoff, dedupe before insert, column-safe truncation, retention-aware ingest, per-user refresh coalescing. - Read/favorite/read-all are single ownership-scoped statements. - gzip compression, immutable caching for hashed assets, path-safe SPA handler, server timeouts; expired sessions purged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
1.5 KiB
Go
59 lines
1.5 KiB
Go
package utils
|
|
|
|
import (
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
func TestPlainText(t *testing.T) {
|
|
got := PlainText(`<p>Bonjour <b>le</b> monde</p><script>alert(1)</script><p>Fin & suite</p>`)
|
|
want := "Bonjour le monde Fin & suite"
|
|
if got != want {
|
|
t.Fatalf("PlainText = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestExcerptCutsOnWordBoundary(t *testing.T) {
|
|
s := strings.Repeat("mot ", 100)
|
|
got := Excerpt(s, 50)
|
|
if !strings.HasSuffix(got, "…") || utf8.RuneCountInString(got) > 51 {
|
|
t.Fatalf("unexpected excerpt %q", got)
|
|
}
|
|
if strings.Contains(got, "mo…") {
|
|
t.Fatalf("excerpt split a word: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestTruncateRunesKeepsUTF8Valid(t *testing.T) {
|
|
got := TruncateRunes("éééé", 2)
|
|
if got != "éé" || !utf8.ValidString(got) {
|
|
t.Fatalf("TruncateRunes = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestReadingMinutes(t *testing.T) {
|
|
cases := map[int]int{0: 1, 1: 1, 238: 1, 239: 2, 2380: 10}
|
|
for words, want := range cases {
|
|
if got := ReadingMinutes(words); got != want {
|
|
t.Errorf("ReadingMinutes(%d) = %d, want %d", words, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsDisallowedIP(t *testing.T) {
|
|
blocked := []string{"127.0.0.1", "10.1.2.3", "192.168.1.1", "169.254.169.254", "::1",
|
|
"64:ff9b::a00:1", "2002:a00:1::1", "198.18.0.1", "100.64.0.1", "::ffff:127.0.0.1"}
|
|
for _, s := range blocked {
|
|
if !isDisallowedIP(net.ParseIP(s)) {
|
|
t.Errorf("%s should be blocked", s)
|
|
}
|
|
}
|
|
for _, s := range []string{"1.1.1.1", "2606:4700:4700::1111"} {
|
|
if isDisallowedIP(net.ParseIP(s)) {
|
|
t.Errorf("%s should be allowed", s)
|
|
}
|
|
}
|
|
}
|