| 1 | package tinyclient |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/FelineStateMachine/tinyrelay/internal/policy" |
| 10 | "github.com/FelineStateMachine/tinyrelay/internal/seedmark" |
| 11 | ) |
| 12 | |
| 13 | func TestAvatarEndpointRendersWithoutProfileAccess(t *testing.T) { |
| 14 | backend := &fakeBackend{policy: policy.Defaults(strings.Repeat("a" , 64))} |
| 15 | backend.policy.Features.Grasp08 = true |
| 16 | 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 | } |
| 42 | |
| 43 | func 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 string |
| 50 | status int |
| 51 | }{ |
| 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 | } |
| 65 | |
| 66 | func 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 | } |