Extract ActivityPub outbox walk into internal/activitypub/outbox.go with webfinger/actor resolution, Create/Note filtering and IRI dereferencing. Fix searchStatus returning wrong post, nil-account panic in SearchActors, @local handle resolution, empty Author via noteToInternalPost, protocol- relative URL handling, pagination loops, limit clamping (200) and cacheErr masking. Add per-host token-bucket throttle (1/s, cross-host concurrency) in throttle.go and background GTS warming via search?resolve for outbox- fetched posts without following remotes. Align AGENTS.md. Co-Authored-By: internal-model
597 lines
20 KiB
Go
597 lines
20 KiB
Go
package activitypub
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
|
|
|
func newTestClient(serverURL string) *APClient {
|
|
ap, err := NewAPClient("https://example.com", "tok")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
host := strings.TrimPrefix(serverURL, "http://")
|
|
ap.client = &http.Client{
|
|
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
u := *r.URL
|
|
u.Scheme = "http"
|
|
u.Host = host
|
|
r2 := r.Clone(r.Context())
|
|
r2.URL = &u
|
|
return http.DefaultTransport.RoundTrip(r2)
|
|
}),
|
|
}
|
|
return ap
|
|
}
|
|
|
|
func TestNewAPClientEmpty(t *testing.T) {
|
|
if _, err := NewAPClient("", "tok"); err == nil {
|
|
t.Fatal("expected error for empty host")
|
|
}
|
|
if _, err := NewAPClient("host", ""); err == nil {
|
|
t.Fatal("expected error for empty apikey")
|
|
}
|
|
}
|
|
|
|
func TestGetLatestNPosts(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","acct":"alice"}]`)
|
|
})
|
|
mux.HandleFunc("/api/v1/accounts/1/statuses", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "Bearer tok" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if r.URL.Query().Get("max_id") != "" {
|
|
_, _ = fmt.Fprint(w, `[]`)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","content":"hello world","text":"hello world","created_at":"2024-01-01T00:00:00Z","account":{"acct":"alice"}}]`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
posts, err := ap.GetLatestNPosts(context.Background(), "alice", 10)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestNPosts error: %v", err)
|
|
}
|
|
if len(posts) != 1 {
|
|
t.Fatalf("expected 1 post, got %d", len(posts))
|
|
}
|
|
if posts[0].Content != "hello world" || posts[0].Author != "alice" {
|
|
t.Fatalf("unexpected post: %+v", posts[0])
|
|
}
|
|
}
|
|
|
|
func TestGetLatestNPostsPaginates(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","acct":"alice"}]`)
|
|
})
|
|
mux.HandleFunc("/api/v1/accounts/1/statuses", func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Query().Get("max_id") {
|
|
case "40":
|
|
ids := make([]map[string]any, 0, 10)
|
|
for id := 41; id <= 50; id++ {
|
|
ids = append(ids, map[string]any{
|
|
"id": fmt.Sprintf("%d", id), "content": fmt.Sprintf("post %d", id), "created_at": "2024-01-01T00:00:00Z",
|
|
"account": map[string]any{"acct": "alice"},
|
|
})
|
|
}
|
|
_ = json.NewEncoder(w).Encode(ids)
|
|
case "":
|
|
ids := make([]map[string]any, 0, 40)
|
|
for id := 1; id <= 40; id++ {
|
|
ids = append(ids, map[string]any{
|
|
"id": fmt.Sprintf("%d", id), "content": fmt.Sprintf("post %d", id), "created_at": "2024-01-01T00:00:00Z",
|
|
"account": map[string]any{"acct": "alice"},
|
|
})
|
|
}
|
|
_ = json.NewEncoder(w).Encode(ids)
|
|
default:
|
|
_ = json.NewEncoder(w).Encode([]map[string]any{})
|
|
}
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
posts, err := ap.GetLatestNPosts(context.Background(), "alice", 50)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestNPosts error: %v", err)
|
|
}
|
|
if len(posts) != 50 {
|
|
t.Fatalf("expected 50 posts across 2 pages, got %d", len(posts))
|
|
}
|
|
if posts[49].Content != "post 50" {
|
|
t.Fatalf("expected last post 'post 50', got %q", posts[49].Content)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestNPostsZeroLimit(t *testing.T) {
|
|
ap := newTestClient("http://example.com")
|
|
posts, err := ap.GetLatestNPosts(context.Background(), "alice", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestNPosts with zero limit error: %v", err)
|
|
}
|
|
if len(posts) != 0 {
|
|
t.Fatalf("expected no posts for zero limit, got %d", len(posts))
|
|
}
|
|
}
|
|
|
|
func TestGetLatestNPostsLimitClamp(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","acct":"alice"}]`)
|
|
})
|
|
mux.HandleFunc("/api/v1/accounts/1/statuses", func(w http.ResponseWriter, r *http.Request) {
|
|
limit := r.URL.Query().Get("limit")
|
|
l, _ := strconv.Atoi(limit)
|
|
if l > maxLimit {
|
|
t.Fatalf("expected limit <= %d, got %s", maxLimit, limit)
|
|
}
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","content":"hello","created_at":"2024-01-01T00:00:00Z","account":{"acct":"alice"}}]`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
posts, err := ap.GetLatestNPosts(context.Background(), "alice", 500)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestNPosts error: %v", err)
|
|
}
|
|
if len(posts) == 0 {
|
|
t.Fatal("expected at least one post")
|
|
}
|
|
}
|
|
|
|
func TestGetPost(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/statuses/42", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `{"id":"42","content":"a post","text":"a post","created_at":"2024-01-01T00:00:00Z","account":{"acct":"bob"}}`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
post, err := ap.GetPost(context.Background(), "42")
|
|
if err != nil {
|
|
t.Fatalf("GetPost error: %v", err)
|
|
}
|
|
if post.Content != "a post" || post.Author != "bob" {
|
|
t.Fatalf("unexpected post: %+v", post)
|
|
}
|
|
}
|
|
|
|
func TestGetPostURLResolves(t *testing.T) {
|
|
url := "https://inst.social/@alice/statuses/42"
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v2/search", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("q") != url {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
if r.URL.Query().Get("type") != "statuses" || r.URL.Query().Get("resolve") != "true" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprint(w, `{"accounts":[],"statuses":[{"id":"42","uri":"`+url+`","url":"`+url+`","content":"resolved post","created_at":"2024-01-01T00:00:00Z","account":{"acct":"bob@inst.social"}}]}`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
post, err := ap.GetPost(context.Background(), url)
|
|
if err != nil {
|
|
t.Fatalf("GetPost error: %v", err)
|
|
}
|
|
if post.Content != "resolved post" || post.Author != "bob@inst.social" {
|
|
t.Fatalf("unexpected post: %+v", post)
|
|
}
|
|
}
|
|
|
|
func TestGetPostURLNotFound(t *testing.T) {
|
|
derefTarget := httptest.NewServer(http.NotFoundHandler())
|
|
defer derefTarget.Close()
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v2/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `{"accounts":[],"statuses":[]}`)
|
|
})
|
|
gts := httptest.NewServer(mux)
|
|
defer gts.Close()
|
|
|
|
ap, err := NewAPClient(gts.URL, "tok")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ap.GetPost(context.Background(), derefTarget.URL+"/missed"); err == nil {
|
|
t.Fatal("expected error when search and dereference both return nothing")
|
|
}
|
|
}
|
|
|
|
func TestGetPostURLSearchReturnsWrongPost(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v2/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `{"accounts":[],"statuses":[{"id":"wrong","uri":"https://other.com/1","content":"wrong post"}]}`)
|
|
})
|
|
gts := httptest.NewServer(mux)
|
|
defer gts.Close()
|
|
|
|
ap, err := NewAPClient(gts.URL, "tok")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ap.GetPost(context.Background(), "https://inst.social/@alice/statuses/42"); err == nil {
|
|
t.Fatal("expected error when search returns no matching post")
|
|
}
|
|
}
|
|
|
|
func TestGetPostURLDerefFallback(t *testing.T) {
|
|
remote, _ := newRemoteServer()
|
|
defer remote.Close()
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v2/search", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
})
|
|
gts := httptest.NewServer(mux)
|
|
defer gts.Close()
|
|
|
|
ap, err := NewAPClient(gts.URL, "tok")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
post, err := ap.GetPost(context.Background(), remote.URL+"/notes/2")
|
|
if err != nil {
|
|
t.Fatalf("GetPost error: %v", err)
|
|
}
|
|
if post.Content != "deref note 2" {
|
|
t.Fatalf("expected dereferenced note, got %+v", post)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestNPostsRemoteFallsBackToOutbox(t *testing.T) {
|
|
remote, remoteBase := newRemoteServer()
|
|
defer remote.Close()
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprintf(w, `[{"id":"1","acct":"alice@example.social","url":%q}]`, remoteBase)
|
|
})
|
|
mux.HandleFunc("/api/v1/accounts/1/statuses", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("max_id") != "" {
|
|
_, _ = fmt.Fprint(w, `[]`)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprint(w, `[{"id":"c1","content":"cached","uri":`+`"`+remoteBase+`/notes/9","created_at":"2024-03-10T00:00:00Z","account":{"acct":"alice@example.social"}}]`)
|
|
})
|
|
gts := httptest.NewServer(mux)
|
|
defer gts.Close()
|
|
|
|
ap, err := NewAPClient(gts.URL, "tok")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
posts, err := ap.GetLatestNPosts(context.Background(), "@alice@example.social", 5)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestNPosts error: %v", err)
|
|
}
|
|
if len(posts) != 5 {
|
|
t.Fatalf("expected 5 merged posts, got %d: %+v", len(posts), posts)
|
|
}
|
|
if posts[0].Content != "cached" {
|
|
t.Fatalf("expected newest cached first, got %q", posts[0].Content)
|
|
}
|
|
if posts[4].Content != "note 4" {
|
|
t.Fatalf("expected oldest outbox post last, got %q", posts[4].Content)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestNPostsRemoteEmptyCacheFallsBack(t *testing.T) {
|
|
remote, remoteBase := newRemoteServer()
|
|
defer remote.Close()
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprintf(w, `[{"id":"1","acct":"alice@example.social","url":%q}]`, remoteBase)
|
|
})
|
|
mux.HandleFunc("/api/v1/accounts/1/statuses", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("max_id") != "" {
|
|
_, _ = fmt.Fprint(w, `[]`)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprint(w, `[]`)
|
|
})
|
|
gts := httptest.NewServer(mux)
|
|
defer gts.Close()
|
|
|
|
ap, err := NewAPClient(gts.URL, "tok")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
posts, err := ap.GetLatestNPosts(context.Background(), "@alice@example.social", 4)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestNPosts error: %v", err)
|
|
}
|
|
if len(posts) != 4 {
|
|
t.Fatalf("expected all 4 posts from outbox, got %d", len(posts))
|
|
}
|
|
if posts[0].Content != "bare note 3" {
|
|
t.Fatalf("expected outbox post newest-first, got %q", posts[0].Content)
|
|
}
|
|
}
|
|
|
|
func TestGetPostsInTimeframeRemoteFallsBackToOutbox(t *testing.T) {
|
|
remote, remoteBase := newRemoteServer()
|
|
defer remote.Close()
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprintf(w, `[{"id":"1","acct":"alice@example.social","url":%q}]`, remoteBase)
|
|
})
|
|
mux.HandleFunc("/api/v1/accounts/1/statuses", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("max_id") != "" {
|
|
_, _ = fmt.Fprint(w, `[]`)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprint(w, `[{"id":"c1","content":"cached in window","uri":`+`"`+remoteBase+`/notes/5","created_at":"2024-02-20T00:00:00Z","account":{"acct":"alice@example.social"}}]`)
|
|
})
|
|
gts := httptest.NewServer(mux)
|
|
defer gts.Close()
|
|
|
|
ap, err := NewAPClient(gts.URL, "tok")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
end := time.Date(2024, 3, 2, 0, 0, 0, 0, time.UTC)
|
|
posts, err := ap.GetPostsInTimeframe(context.Background(), "@alice@example.social", start, end, 3)
|
|
if err != nil {
|
|
t.Fatalf("GetPostsInTimeframe error: %v", err)
|
|
}
|
|
if len(posts) != 3 {
|
|
t.Fatalf("expected 3 in-window posts, got %d: %+v", len(posts), posts)
|
|
}
|
|
if posts[0].Content != "created note 1" || posts[1].Content != "cached in window" || posts[2].Content != "deref note 2" {
|
|
t.Fatalf("unexpected merged order: %+v", posts)
|
|
}
|
|
}
|
|
|
|
func TestGetPostsInTimeframe(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","acct":"alice"}]`)
|
|
})
|
|
mux.HandleFunc("/api/v1/accounts/1/statuses", func(w http.ResponseWriter, r *http.Request) {
|
|
feed := []map[string]any{}
|
|
switch r.URL.Query().Get("max_id") {
|
|
case "":
|
|
feed = []map[string]any{
|
|
{"id": "5", "content": "too-new", "text": "too-new", "created_at": "2025-03-01T00:00:00Z", "account": map[string]any{"acct": "alice"}},
|
|
{"id": "4", "content": "mid", "text": "mid", "created_at": "2024-06-01T00:00:00Z", "account": map[string]any{"acct": "alice"}},
|
|
}
|
|
case "4":
|
|
feed = []map[string]any{
|
|
{"id": "3", "content": "early-mid", "text": "early-mid", "created_at": "2024-03-01T00:00:00Z", "account": map[string]any{"acct": "alice"}},
|
|
{"id": "2", "content": "too-old", "text": "too-old", "created_at": "2023-01-01T00:00:00Z", "account": map[string]any{"acct": "alice"}},
|
|
}
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(feed); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
end := time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)
|
|
posts, err := ap.GetPostsInTimeframe(context.Background(), "alice", start, end, 5)
|
|
if err != nil {
|
|
t.Fatalf("GetPostsInTimeframe error: %v", err)
|
|
}
|
|
if len(posts) != 2 {
|
|
t.Fatalf("expected 2 in-window posts, got %d", len(posts))
|
|
}
|
|
if posts[0].Content != "mid" || posts[1].Content != "early-mid" {
|
|
t.Fatalf("unexpected posts: %+v", posts)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestNPostsHTTPError(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","acct":"alice"}]`)
|
|
})
|
|
mux.HandleFunc("/api/v1/accounts/1/statuses", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
if _, err := ap.GetLatestNPosts(context.Background(), "alice", 10); err == nil {
|
|
t.Fatal("expected error on non-200 status from timeline")
|
|
}
|
|
}
|
|
|
|
func TestGetActor(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("q") != "alice" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","acct":"alice@inst.social","display_name":"Alice","note":"hello there","avatar":"https://img/avatar.png","followers_count":10,"following_count":5,"statuses_count":3,"url":"https://inst.social/@alice"}]`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
prof, err := ap.GetActor(context.Background(), "alice")
|
|
if err != nil {
|
|
t.Fatalf("GetActor error: %v", err)
|
|
}
|
|
if prof.Platform != "ActivityPub" || prof.Handle != "alice@inst.social" || prof.DisplayName != "Alice" {
|
|
t.Fatalf("unexpected profile: %+v", prof)
|
|
}
|
|
if prof.Bio != "hello there" || prof.AvatarURL != "https://img/avatar.png" {
|
|
t.Fatalf("unexpected profile fields: %+v", prof)
|
|
}
|
|
if prof.FollowersCount != 10 || prof.FollowingCount != 5 || prof.PostCount != 3 {
|
|
t.Fatalf("unexpected counts: %+v", prof)
|
|
}
|
|
if prof.URL != "https://inst.social/@alice" {
|
|
t.Fatalf("unexpected url: %q", prof.URL)
|
|
}
|
|
}
|
|
|
|
func TestGetActorIDFallback(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `[]`)
|
|
})
|
|
mux.HandleFunc("/api/v1/accounts/alice", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `{"id":"1","acct":"alice@inst.social","display_name":"Alice","note":"hi","avatar":"https://img/a.png","followers_count":1,"following_count":2,"statuses_count":3,"url":"https://x"}`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
prof, err := ap.GetActor(context.Background(), "alice")
|
|
if err != nil {
|
|
t.Fatalf("GetActor error: %v", err)
|
|
}
|
|
if prof.Handle != "alice@inst.social" {
|
|
t.Fatalf("expected fallback id lookup to succeed, got %+v", prof)
|
|
}
|
|
}
|
|
|
|
func TestGetActorAtLocalHandle(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query().Get("q")
|
|
if q != "@alice" && q != "alice" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","acct":"alice","display_name":"Alice","note":"hi","avatar":"https://img/a.png","followers_count":1,"following_count":2,"statuses_count":3,"url":"https://x"}]`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
prof, err := ap.GetActor(context.Background(), "@alice")
|
|
if err != nil {
|
|
t.Fatalf("GetActor error: %v", err)
|
|
}
|
|
if prof.Handle != "alice" {
|
|
t.Fatalf("expected local handle, got %q", prof.Handle)
|
|
}
|
|
}
|
|
|
|
func TestSearchActors(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("q") != "alice" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
if r.URL.Query().Get("resolve") != "true" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","acct":"alice@inst.social","display_name":"Alice","note":"bio","avatar":"https://img/a.png","followers_count":1,"following_count":2,"statuses_count":3,"url":"https://x"}]`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
results, err := ap.SearchActors(context.Background(), "alice", 10)
|
|
if err != nil {
|
|
t.Fatalf("SearchActors error: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Fatalf("expected 1 result, got %d", len(results))
|
|
}
|
|
if results[0].Platform != "ActivityPub" || results[0].Handle != "alice@inst.social" {
|
|
t.Fatalf("unexpected result: %+v", results[0])
|
|
}
|
|
}
|
|
|
|
func TestExtractHandleFromURL(t *testing.T) {
|
|
tests := []struct {
|
|
url string
|
|
expected string
|
|
}{
|
|
{"https://inst.social/@alice/statuses/42", "alice"},
|
|
{"http://example.com/@bob/posts/1", "bob"},
|
|
{"https://no-at-here.com/notes/1", ""},
|
|
{"https://host/@user/note", "user"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := extractHandleFromURL(tt.url)
|
|
if got != tt.expected {
|
|
t.Fatalf("extractHandleFromURL(%q) = %q, want %q", tt.url, got, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetPostBareLocalHandle(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query().Get("q")
|
|
if q != "@alice" && q != "alice" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprint(w, `[{"id":"1","acct":"alice"}]`)
|
|
})
|
|
mux.HandleFunc("/api/v1/statuses/alice", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `{"id":"1","content":"local post","created_at":"2024-01-01T00:00:00Z","account":{"acct":"alice"}}`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
_, err := ap.GetPost(context.Background(), "alice")
|
|
if err != nil {
|
|
t.Fatalf("GetPost with local handle error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSearchActorsNilEntries(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/accounts/search", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = fmt.Fprint(w, `[null,{"id":"1","acct":"alice@inst.social"}]`)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
ap := newTestClient(server.URL)
|
|
results, err := ap.SearchActors(context.Background(), "alice", 10)
|
|
if err != nil {
|
|
t.Fatalf("SearchActors error: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Fatalf("expected 1 result after filtering nil, got %d", len(results))
|
|
}
|
|
if results[0].Handle != "alice@inst.social" {
|
|
t.Fatalf("unexpected result: %+v", results[0])
|
|
}
|
|
}
|