commit ca8ce61dba115340a2a31b8fd9c9f09a4f5e0b1c
parent 9f1e7414b8559511c3792a25cf11b4ce7971bda3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 26 Aug 2026 05:25:28 -0700
Test TUI clients against key-auth server
Diffstat:
3 files changed, 156 insertions(+), 0 deletions(-)
diff --git a/dev/e2e-key-server b/dev/e2e-key-server
@@ -0,0 +1,37 @@
+#!/bin/sh
+set -eu
+
+repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
+server_repo=$(CDPATH= cd -- "$repo/../gotextlog-srv" && pwd)
+tmp=$(mktemp -d)
+pid=
+cleanup() {
+ if [ -n "$pid" ]; then
+ kill "$pid" 2>/dev/null || true
+ wait "$pid" 2>/dev/null || true
+ fi
+ rm -rf "$tmp"
+}
+trap cleanup EXIT INT TERM
+
+port=18937
+url="http://127.0.0.1:$port"
+(
+ cd "$server_repo"
+ CGO_ENABLED=1 go build -mod=vendor -o "$tmp/gotextlog-srv" ./cmd/gotextlog-srv
+)
+"$tmp/gotextlog-srv" -listen "127.0.0.1:$port" -public-origin "$url" -database "$tmp/textlog.db" >"$tmp/server.log" 2>&1 &
+pid=$!
+
+attempt=0
+while ! curl -fsS "$url/api/v1/feeds/latest?limit=1" >/dev/null 2>&1; do
+ attempt=$((attempt + 1))
+ if [ "$attempt" -ge 100 ]; then
+ cat "$tmp/server.log" >&2
+ exit 1
+ fi
+ sleep 0.05
+done
+
+cd "$repo"
+GOTEXTLOG_TEST_SERVER_URL="$url" go test -race ./internal/tui -run TestServerEndToEndWithTwoTUIBackends -count=1
diff --git a/internal/tui/server_e2e_test.go b/internal/tui/server_e2e_test.go
@@ -0,0 +1,115 @@
+package tui
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "git.ryansepassi.com/git/gotextlog.git/internal/sshkey"
+ "git.ryansepassi.com/git/gotextlog.git/internal/textlog"
+ "golang.org/x/crypto/ssh"
+)
+
+type generatedKeySource struct {
+ key sshkey.Key
+ signer ssh.Signer
+}
+
+func newGeneratedKeySource(t *testing.T) *generatedKeySource {
+ t.Helper()
+ _, privateKey, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ signer, err := ssh.NewSignerFromKey(privateKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ public := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))
+ return &generatedKeySource{
+ key: sshkey.Key{PublicKey: public, Fingerprint: ssh.FingerprintSHA256(signer.PublicKey())},
+ signer: signer,
+ }
+}
+
+func (s *generatedKeySource) Keys(context.Context) ([]sshkey.Key, error) {
+ return []sshkey.Key{s.key}, nil
+}
+
+func (s *generatedKeySource) Sign(_ context.Context, _ sshkey.Key, message []byte) ([]byte, error) {
+ signature, err := s.signer.Sign(rand.Reader, message)
+ if err != nil {
+ return nil, err
+ }
+ return ssh.Marshal(signature), nil
+}
+
+func TestServerEndToEndWithTwoTUIBackends(t *testing.T) {
+ baseURL := os.Getenv("GOTEXTLOG_TEST_SERVER_URL")
+ if baseURL == "" {
+ t.Skip("set GOTEXTLOG_TEST_SERVER_URL to run against gotextlog-srv")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+ defer cancel()
+
+ aliceKeys := newGeneratedKeySource(t)
+ bobKeys := newGeneratedKeySource(t)
+ alice := NewClientBackendWithKeySource(textlog.NewClient(baseURL, "", nil), aliceKeys)
+ bob := NewClientBackendWithKeySource(textlog.NewClient(baseURL, "", nil), bobKeys)
+
+ aliceLogin, err := alice.Execute(ctx, Operation{Kind: OpKeyLogin, Key: aliceKeys.key, Handle: "alice"})
+ if err != nil || aliceLogin.Me == nil || aliceLogin.Me.Handle != "alice" {
+ t.Fatalf("register alice: result=%#v err=%v", aliceLogin, err)
+ }
+ bobLogin, err := bob.Execute(ctx, Operation{Kind: OpKeyLogin, Key: bobKeys.key, Handle: "bob"})
+ if err != nil || bobLogin.Me == nil || bobLogin.Me.Handle != "bob" {
+ t.Fatalf("register bob: result=%#v err=%v", bobLogin, err)
+ }
+
+ if _, err := bob.Execute(ctx, Operation{Kind: OpFollow, Handle: "alice", Enabled: true}); err != nil {
+ t.Fatalf("bob follows alice: %v", err)
+ }
+ created, err := alice.Execute(ctx, Operation{Kind: OpCreatePost, Text: "hello from #golang to @bob"})
+ if err != nil || created.Post == nil {
+ t.Fatalf("alice creates post: result=%#v err=%v", created, err)
+ }
+ rootID := created.Post.ID
+ reply, err := bob.Execute(ctx, Operation{Kind: OpCreatePost, Text: "hello back", ParentID: &rootID})
+ if err != nil || reply.Post == nil || reply.Post.ParentID == nil || *reply.Post.ParentID != rootID {
+ t.Fatalf("bob replies: result=%#v err=%v", reply, err)
+ }
+
+ thread, err := alice.Execute(ctx, Operation{Kind: OpThread, ID: rootID})
+ if err != nil || len(thread.Replies) != 1 || thread.Replies[0].ID != reply.Post.ID {
+ t.Fatalf("thread: result=%#v err=%v", thread, err)
+ }
+ feed, err := bob.Execute(ctx, Operation{Kind: OpFeed, Feed: "for-you"})
+ if err != nil || len(feed.Entries) == 0 {
+ t.Fatalf("bob for-you feed: result=%#v err=%v", feed, err)
+ }
+ toAlice, err := alice.Execute(ctx, Operation{Kind: OpFeed, Feed: "to-me"})
+ if err != nil || len(toAlice.Entries) < 2 {
+ t.Fatalf("alice activity: result=%#v err=%v", toAlice, err)
+ }
+ search, err := bob.Execute(ctx, Operation{Kind: OpSearch, Query: "golang"})
+ if err != nil || len(search.Posts) != 1 || search.Posts[0].ID != rootID {
+ t.Fatalf("search: result=%#v err=%v", search, err)
+ }
+ profile, err := bob.Execute(ctx, Operation{Kind: OpProfile, Handle: "alice"})
+ if err != nil || profile.Profile == nil || profile.Profile.PostCount != 1 {
+ t.Fatalf("profile: result=%#v err=%v", profile, err)
+ }
+ if _, err := bob.Execute(ctx, Operation{Kind: OpBlock, Handle: "alice", Enabled: true}); err != nil {
+ t.Fatalf("bob blocks alice: %v", err)
+ }
+ if _, err := bob.Execute(ctx, Operation{Kind: OpRevoke}); err != nil {
+ t.Fatalf("bob revokes session: %v", err)
+ }
+ if _, err := bob.Execute(ctx, Operation{Kind: OpMe}); err == nil {
+ t.Fatal("revoked session still authenticated")
+ }
+}
diff --git a/justfile b/justfile
@@ -14,3 +14,7 @@ clean:
# Format all Go packages.
format:
go fmt ./...
+
+# Run the SSH key authentication flow against the sibling server.
+e2e-key-server:
+ ./dev/e2e-key-server