clone

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

feat/standalone-relay
tinyrelay / tinyclient / avatars_test.go
avatars_test.go 3.2K
1package tinyclient23import (4 "net/http"5 "net/http/httptest"6 "strings"7 "testing"89 "github.com/FelineStateMachine/tinyrelay/internal/policy"10 "github.com/FelineStateMachine/tinyrelay/internal/seedmark"11)1213func TestAvatarEndpointRendersWithoutProfileAccess(t *testing.T) {14 backend := &fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}15 backend.policy.Features.Grasp08 = true16 app, err := New(backend, Options{})17 if err != nil {18 t.Fatal(err)19 }20 key := strings.Repeat("b", 64)21 path := avatarURL(key)22 response := httptest.NewRecorder()23 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))24 if response.Code != http.StatusOK || response.Body.String() != seedmark.Avatar(key) {25 t.Fatalf("avatar: status=%d body=%s", response.Code, response.Body.String())26 }27 if backend.call != "" || response.Header().Get("Content-Type") != "image/svg+xml; charset=utf-8" || !strings.Contains(response.Header().Get("Cache-Control"), "immutable") {28 t.Fatalf("avatar accessed relay data or lacks image caching: call=%q headers=%v", backend.call, response.Header())29 }30 for _, method := range []string{http.MethodHead, http.MethodGet} {31 request := httptest.NewRequest(method, path, nil)32 if method == http.MethodGet {33 request.Header.Set("If-None-Match", response.Header().Get("ETag"))34 }35 next := httptest.NewRecorder()36 app.ServeHTTP(next, request)37 if next.Body.Len() != 0 || (method == http.MethodHead && next.Code != http.StatusOK) || (method == http.MethodGet && next.Code != http.StatusNotModified) {38 t.Fatalf("%s cache response: status=%d body=%s", method, next.Code, next.Body.String())39 }40 }41}4243func TestAvatarEndpointRejectsOtherVersionsSeedsAndWrites(t *testing.T) {44 app, err := New(&fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}, Options{})45 if err != nil {46 t.Fatal(err)47 }48 for _, tc := range []struct {49 method, path string50 status int51 }{52 {http.MethodGet, "/avatars/v2/" + strings.Repeat("a", 64) + ".svg", http.StatusNotFound},53 {http.MethodGet, "/avatars/v1/short.svg", http.StatusNotFound},54 {http.MethodGet, "/avatars/v1/" + strings.Repeat("g", 64) + ".svg", http.StatusNotFound},55 {http.MethodGet, "/avatars/v1/" + strings.Repeat("a", 64) + ".png", http.StatusNotFound},56 {http.MethodPost, avatarURL(strings.Repeat("a", 64)), http.StatusMethodNotAllowed},57 } {58 response := httptest.NewRecorder()59 app.ServeHTTP(response, httptest.NewRequest(tc.method, tc.path, nil))60 if response.Code != tc.status {61 t.Errorf("%s %s: status=%d, want %d", tc.method, tc.path, response.Code, tc.status)62 }63 }64}6566func TestAvatarSeedsUseFullKeysAndRoomIdentity(t *testing.T) {67 key := strings.Repeat("ab", 32)68 if avatarURL(strings.ToUpper(key)) != avatarURL(key) || avatarURL(key) == avatarURL(key[:62]+"cd") {69 t.Fatal("profile seed must use the full normalized key")70 }71 if avatarURL("invalid") != "/avatars/v1/"+strings.Repeat("0", 64)+".svg" {72 t.Fatal("invalid keys should use the anonymous fallback")73 }74 room := roomAvatarURL("https://relay.test/r/team", "general")75 if room != roomAvatarURL("https://relay.test/r/team/", "general") || room == roomAvatarURL("https://relay.test/r/other", "general") || room == roomAvatarURL("https://relay.test/r/team", "random") {76 t.Fatal("room avatars must follow stable relay and room identity")77 }78}