Files
summercms/modules/lighthouse/channel_test.go
Jakub Zych 79fd705680 feat(11-03): re-authorize every Centrifugo subscribe through a namespace registry
- lighthouse: Registry of namespace authorizers (Result, Allowed, Denied),
  ParseChannel, ChannelID with PHP (int)-cast semantics (PHPInt, pinned by
  a php -r table test), FormatChannels, WithClientID/ClientID
- centrifugo: ProxyHandler (constant-time X-Centrifugo-Secret, HTTP 200
  generic deny, info [] on allow, presence allow/override merge, 64 KiB
  body cap) mounted as the ServerToServer subscribe route
- README: proxy contract, registry and channel rules
2026-09-30 12:29:09 +02:00

121 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lighthouse
import (
"context"
"math"
"reflect"
"testing"
)
// TestChannelIDMatchesPHP pins ChannelID to PHP's (int) cast of the id
// segment. The expected values were printed by PHP 8.5.10:
//
// php -r 'foreach ([...] as $s) var_dump((int)$s);'
func TestChannelIDMatchesPHP(t *testing.T) {
cases := []struct {
segment string
want int64
}{
{"5", 5}, {"05", 5}, {" 5", 5}, {"5abc", 5}, {"abc", 0}, {"-3", -3}, {"", 0},
{"1e3", 1000}, {"0x1A", 0}, {"9999999999999999999", math.MaxInt64},
{"\t5", 5}, {"\n5", 5}, {"5 ", 5}, {"+5", 5}, {" -7x", -7}, {"1.9", 1}, {"-0", 0},
{"1e20", math.MaxInt64}, {"0b11", 0}, {"٥", 0}, {" ", 0},
{"-9999999999999999999", math.MinInt64}, {"-1e20", math.MinInt64}, {"1e-3", 0},
{".5", 0}, {"5.", 5}, {"1e", 1}, {"1e+2", 100}, {"\v5", 5}, {"\f5", 5}, {"\r5", 5},
{"NaN", 0}, {"INF", 0}, {"1E3", 1000}, {"9223372036854775807", math.MaxInt64},
{"9223372036854775808", math.MaxInt64}, {"1.5e3", 1500}, {"-.5e1", -5}, {"0005", 5},
{"00", 0}, {"- 5", 0}, {"5e3abc", 5000}, {"12.7e1x", 127}, {"1e400", 0},
}
for _, tc := range cases {
if got := ChannelID("collection:" + tc.segment); got != tc.want {
t.Errorf("ChannelID(collection:%q) = %d, want %d", tc.segment, got, tc.want)
}
}
if got := ChannelID("collection"); got != 0 {
t.Errorf("missing segment = %d, want 0", got)
}
if got := ChannelID("presence:collection:5"); got != 0 {
t.Errorf("presence:collection:5 = %d, want 0 (segment 1 is \"collection\")", got)
}
if got := ChannelID("wishlist:12:extra"); got != 12 {
t.Errorf("wishlist:12:extra = %d, want 12", got)
}
}
func TestParseChannel(t *testing.T) {
cases := []struct {
channel string
namespace string
presence bool
}{
{"collection:5", "collection", false},
{"ns:entity:id", "ns", false},
{"presence:acme:room:1", "acme", true},
{"presence:collection:5", "collection", true},
{"presence:presence:x", "", true},
{"a:b:c:d", "", false},
{"presence:a:b:c:d", "", true},
{"", "", false},
{":5", "", false},
{"Collection:5", "Collection", false},
}
for _, tc := range cases {
ns, presence := ParseChannel(tc.channel)
if ns != tc.namespace || presence != tc.presence {
t.Errorf("ParseChannel(%q) = (%q, %t), want (%q, %t)", tc.channel, ns, presence, tc.namespace, tc.presence)
}
}
}
func TestFormatChannels(t *testing.T) {
got := FormatChannels("Acme", []string{"Collection:5", "acme:user:1", "ACME:x"})
want := []string{"acme:collection:5", "acme:user:1", "acme:x"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("FormatChannels = %v, want %v", got, want)
}
if got := FormatChannels("", []string{"Collection:5"}); !reflect.DeepEqual(got, []string{"collection:5"}) {
t.Fatalf("no namespace = %v", got)
}
}
func TestClientID(t *testing.T) {
if ClientID(context.Background()) != "" {
t.Fatal("empty ctx has a client id")
}
if got := ClientID(WithClientID(context.Background(), "c-1")); got != "c-1" {
t.Fatalf("ClientID = %q", got)
}
}
func TestRegistry(t *testing.T) {
r := NewRegistry()
allow := AuthorizerFunc(func(context.Context, uint, string) Result { return Allowed(nil) })
if err := r.Register("b", allow); err != nil {
t.Fatal(err)
}
if err := r.Register("a", allow); err != nil {
t.Fatal(err)
}
for _, bad := range []struct {
ns string
a Authorizer
}{{"", allow}, {"x:y", allow}, {"c", nil}, {"a", allow}} {
if err := r.Register(bad.ns, bad.a); err == nil {
t.Errorf("Register(%q) accepted", bad.ns)
}
}
if got := r.Namespaces(); !reflect.DeepEqual(got, []string{"a", "b"}) {
t.Fatalf("Namespaces = %v", got)
}
if _, ok := r.Get("A"); ok {
t.Fatal("lookup is not case-sensitive")
}
if a, ok := r.Get("a"); !ok || !a.Authorize(context.Background(), 1, "a:1").Allowed {
t.Fatal("Get(a) failed")
}
d := Denied("why")
if d.Allowed || d.Reason() != "why" {
t.Fatalf("Denied = %+v", d)
}
}