gotextlog

gotextlog
git clone https://git.ryansepassi.com/git/gotextlog.git
Log | Files | Refs | README

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:
Adev/e2e-key-server | 37+++++++++++++++++++++++++++++++++++++
Ainternal/tui/server_e2e_test.go | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mjustfile | 4++++
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