clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / tinyclient / social_profile_test.go
social_profile_test.go 5.2K
1package tinyclient23import (4 "context"5 "encoding/json"6 "errors"7 "net/http"8 "net/http/httptest"9 "strings"10 "testing"1112 "github.com/FelineStateMachine/tinyrelay/internal/policy"13)1415func TestSocialProfileAuthorAcceptsHexAndNpub(t *testing.T) {16 hexKey := strings.Repeat("ab", 32)17 if got, err := socialProfileAuthor(hexKey); err != nil || got != hexKey {18 t.Fatalf("hex author = %q, %v", got, err)19 }20 npub := "npub1424242424242424242424242424242424242424242424242424qamrcaj"21 if got, err := socialProfileAuthor(npub); err != nil || got != strings.Repeat("a", 64) {22 t.Fatalf("npub author = %q, %v", got, err)23 }24}2526func TestSocialProfileAuthorRejectsInvalidKeys(t *testing.T) {27 for _, value := range []string{"", "alice", strings.Repeat("g", 64), "npub1invalid"} {28 if got, err := socialProfileAuthor(value); err == nil || got != "" {29 t.Errorf("author %q accepted as %q", value, got)30 }31 }32}3334type socialProfileTestBackend struct {35 fakeBackend36 queries []map[string]any37 failMethod string38}3940func (b *socialProfileTestBackend) Query(_ context.Context, method string, params []json.RawMessage, _ string) (any, error) {41 var query map[string]any42 _ = json.Unmarshal(params[0], &query)43 b.queries = append(b.queries, query)44 if method == b.failMethod {45 return nil, errors.New("restricted: profile access denied")46 }47 switch method {48 case "browseprofile":49 return map[string]any{"profile": map[string]any{"name": "River", "website": "https://river.example"}}, nil50 case "browsesocial":51 return map[string]any{"items": []any{map[string]any{"id": "post", "kind": 1, "pubkey": query["author"], "content": "From River"}}, "next_cursor": "next"}, nil52 default:53 return nil, nil54 }55}5657func TestSocialProfilePageUsesActorAndAuthorScopedFeed(t *testing.T) {58 owner := strings.Repeat("a", 64)59 b := &socialProfileTestBackend{fakeBackend: fakeBackend{policy: policy.Defaults(owner)}}60 a, err := New(b, Options{Actor: func(*http.Request) (string, error) { return owner, nil }})61 if err != nil {62 t.Fatal(err)63 }64 w := httptest.NewRecorder()65 a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/social/profile?kind=notes", nil))66 body := w.Body.String()67 if w.Code != http.StatusOK || !strings.Contains(body, "River") || !strings.Contains(body, "From River") || !strings.Contains(body, "Older posts") || !strings.Contains(body, "/profile") {68 t.Fatalf("own profile: %d %s", w.Code, body)69 }70 if len(b.queries) != 2 || b.queries[0]["pubkey"] != owner || b.queries[1]["author"] != owner || b.queries[1]["kind"] != "notes" {71 t.Fatalf("profile queries: %#v", b.queries)72 }73}7475func TestSocialProfilePageGuestCanViewOtherAndMustChooseOwn(t *testing.T) {76 other := strings.Repeat("b", 64)77 b := &socialProfileTestBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}}78 a, err := New(b, Options{Actor: func(*http.Request) (string, error) { return "", nil }})79 if err != nil {80 t.Fatal(err)81 }82 w := httptest.NewRecorder()83 a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/social/profile?author="+other, nil))84 if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "From River") || strings.Contains(w.Body.String(), "Edit profile") {85 t.Fatalf("guest profile: %d %s", w.Code, w.Body.String())86 }87 w = httptest.NewRecorder()88 a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/social/profile", nil))89 if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "Sign in") || !strings.Contains(w.Body.String(), "View profile") {90 t.Fatalf("guest profile chooser: %d %s", w.Code, w.Body.String())91 }92}9394func TestSocialProfileErrorsDoNotRenderProfileOrFeed(t *testing.T) {95 for _, method := range []string{"browseprofile", "browsesocial"} {96 t.Run(method, func(t *testing.T) {97 key := strings.Repeat("a", 64)98 b := &socialProfileTestBackend{fakeBackend: fakeBackend{policy: policy.Defaults(key)}, failMethod: method}99 a, err := New(b, Options{})100 if err != nil {101 t.Fatal(err)102 }103 w := httptest.NewRecorder()104 a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/social/profile?author="+key, nil))105 body := w.Body.String()106 if w.Code != http.StatusForbidden || !strings.Contains(body, "profile access denied") || strings.Contains(body, `id="social-profile-head"`) || strings.Contains(body, "No posts yet") {107 t.Fatalf("failed profile response: %d %s", w.Code, body)108 }109 })110 }111}112113func TestSocialProfileOtherAuthorKeepsPaginationAndHidesEditing(t *testing.T) {114 owner, other := strings.Repeat("a", 64), strings.Repeat("b", 64)115 b := &socialProfileTestBackend{fakeBackend: fakeBackend{policy: policy.Defaults(owner)}}116 a, err := New(b, Options{Actor: func(*http.Request) (string, error) { return owner, nil }})117 if err != nil {118 t.Fatal(err)119 }120 w := httptest.NewRecorder()121 a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/social/profile?author="+other+"&kind=photos&q=river&cursor=old", nil))122 body := w.Body.String()123 if w.Code != http.StatusOK || strings.Contains(body, "Edit profile") || !strings.Contains(body, `/social/profile?author=`+other+`&cursor=next&kind=photos&q=river`) {124 t.Fatalf("other profile pagination: %d %s", w.Code, body)125 }126 if b.queries[1]["author"] != other || b.queries[1]["cursor"] != "old" || b.queries[1]["kind"] != "photos" {127 t.Fatalf("author-scoped page: %#v", b.queries)128 }129}