feat: members-only Usenet (NNTP) + Forgejo git provisioning + founding-lifetime $99

WIP feature branch: NNTPS news server, per-member Forgejo accounts on email
confirm, and founding-lifetime pricing tier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-14 14:26:23 +00:00
parent 2a9d841ddb
commit 3b6b9a4a78
20 changed files with 2842 additions and 2 deletions

View file

@ -0,0 +1,111 @@
package news
import (
"context"
"net"
"path/filepath"
"strings"
"testing"
nntpclient "github.com/dustin/go-nntp/client"
"github.com/profullstack/agentbbs/internal/store"
)
// startServer brings up a news Server over an ephemeral loopback listener and
// returns its address. The server stops when the test ends.
func startServer(t *testing.T) (string, store.Store) {
t.Helper()
st, err := store.Open(filepath.Join(t.TempDir(), "t.db"))
if err != nil {
t.Fatalf("open store: %v", err)
}
t.Cleanup(func() { _ = st.Close() })
ns := New(st, "news.test")
if err := ns.SeedGroups(nil); err != nil {
t.Fatalf("seed: %v", err)
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
go func() { _ = ns.Serve(ctx, ln) }()
return ln.Addr().String(), st
}
func TestServerEndToEnd(t *testing.T) {
addr, st := startServer(t)
if _, err := st.EnsureUser("alice", "member", "SHA256:a"); err != nil {
t.Fatalf("ensure user: %v", err)
}
// A non-member cannot authenticate.
if _, err := Dial(addr, "intruder"); err == nil {
t.Fatal("non-member should not be able to connect")
}
// An unauthenticated raw client must not be able to LIST (members-only).
raw, err := nntpclient.New("tcp", addr)
if err != nil {
t.Fatalf("raw dial: %v", err)
}
if _, err := raw.List("active"); err == nil {
t.Fatal("unauthenticated LIST must be refused")
}
_ = raw.Close()
// A member connects, posts, and reads it back.
r, err := Dial(addr, "alice")
if err != nil {
t.Fatalf("member dial: %v", err)
}
defer r.Close()
if err := r.Post("pfs.general", "First post", "", "Hello, Usenet.\n"); err != nil {
t.Fatalf("post: %v", err)
}
groups, err := r.Groups()
if err != nil {
t.Fatalf("groups: %v", err)
}
var general bool
for _, g := range groups {
if g.Name == "pfs.general" {
general = true
}
}
if !general {
t.Fatalf("pfs.general not listed: %+v", groups)
}
g, err := r.Select("pfs.general")
if err != nil {
t.Fatalf("select: %v", err)
}
if g.High < 1 {
t.Fatalf("expected at least one article, high=%d", g.High)
}
ov, err := r.Overview(g.Low, g.High)
if err != nil {
t.Fatalf("overview: %v", err)
}
if len(ov) != 1 || ov[0].Subject != "First post" {
t.Fatalf("overview wrong: %+v", ov)
}
// From is stamped to the member, not anything client-supplied.
if !strings.Contains(ov[0].From, "alice") {
t.Fatalf("From not stamped: %q", ov[0].From)
}
art, err := r.Article(ov[0].Num)
if err != nil {
t.Fatalf("article: %v", err)
}
if !strings.Contains(art, "Hello, Usenet.") || !strings.Contains(art, "Subject: First post") {
t.Fatalf("article body/headers wrong: %q", art)
}
}