snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:17:21 +05:00
commit b352a9a30d
472 changed files with 108375 additions and 0 deletions

12
internal/tools/ascii.go Normal file
View File

@@ -0,0 +1,12 @@
package tools
import "unicode"
func IsASCII(s string) bool {
for _, c := range s {
if c > unicode.MaxASCII {
return false
}
}
return true
}

View File

@@ -0,0 +1,35 @@
package tools
import "testing"
func Test_IsASCII(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{
name: "empty string",
input: "",
expected: true,
},
{
name: "ascii string",
input: "hello",
expected: true,
},
{
name: "non-ascii string",
input: "こんにちは",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if IsASCII(tt.input) != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, !tt.expected)
}
})
}
}

View File

@@ -0,0 +1,41 @@
package tools
import (
"net/http"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
"github.com/centrifugal/centrifuge"
)
func ConnectErrorToToHTTPResponse(err error, transforms []configtypes.ConnectCodeToHTTPResponseTransform) (configtypes.TransformedConnectErrorHttpResponse, bool) {
var code uint32
var body string
switch t := err.(type) {
case *centrifuge.Disconnect:
code = t.Code
body = t.Reason
case centrifuge.Disconnect:
code = t.Code
body = t.Reason
case *centrifuge.Error:
code = t.Code
body = t.Message
default:
}
if code > 0 {
for _, t := range transforms {
if t.Code != code {
continue
}
if t.To.Body == "" {
t.To.Body = body
}
return t.To, true
}
}
return configtypes.TransformedConnectErrorHttpResponse{
StatusCode: http.StatusInternalServerError,
Body: http.StatusText(http.StatusInternalServerError),
}, false
}

30
internal/tools/compare.go Normal file
View File

@@ -0,0 +1,30 @@
package tools
import "crypto/subtle"
// SecureCompare use constant time function to compare the two given array.
func SecureCompare(given, actual []byte) bool {
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
return subtle.ConstantTimeCompare(given, actual) == 1
}
// Securely compare actual to itself to keep constant time, but always return false
if subtle.ConstantTimeCompare(actual, actual) == 1 {
return false
}
return false
}
// SecureCompareString use constant time function to compare the two given string.
func SecureCompareString(given, actual string) bool {
// The following code is incorrect:
// return SecureCompare([]byte(given), []byte(actual))
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
}
// Securely compare actual to itself to keep constant time, but always return false
if subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 {
return false
}
return false
}

View File

@@ -0,0 +1,91 @@
package tools
import "testing"
func TestSecureCompare(t *testing.T) {
type args struct {
given []byte
actual []byte
}
tests := []struct {
name string
args args
want bool
}{
{
name: "eq",
args: args{
given: []byte("test"),
actual: []byte("test"),
},
want: true,
},
{
name: "ne",
args: args{
given: []byte("boom"),
actual: []byte("boot"),
},
want: false,
},
{
name: "ne_len",
args: args{
given: []byte("booms"),
actual: []byte("boom"),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := SecureCompare(tt.args.given, tt.args.actual); got != tt.want {
t.Errorf("SecureCompare() = %v, want %v", got, tt.want)
}
})
}
}
func TestSecureCompareString(t *testing.T) {
type args struct {
given string
actual string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "eq",
args: args{
given: "test",
actual: "test",
},
want: true,
},
{
name: "ne",
args: args{
given: "boom",
actual: "boot",
},
want: false,
},
{
name: "ne_len",
args: args{
given: "booms",
actual: "boom",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := SecureCompareString(tt.args.given, tt.args.actual); got != tt.want {
t.Errorf("SecureCompareString() = %v, want %v", got, tt.want)
}
})
}
}

141
internal/tools/config.go Normal file
View File

@@ -0,0 +1,141 @@
package tools
import (
"bytes"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"text/template"
)
// PathExists returns whether the given file or directory exists or not
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
var jsonConfigTemplate = `{
"client": {
"token": {
"hmac_secret_key": "{{.TokenSecret}}"
},
"allowed_origins": []
},
"admin": {
"enabled": false,
"password": "{{.AdminPassword}}",
"secret": "{{.AdminSecret}}"
},
"http_api": {
"key": "{{.APIKey}}"
}
}
`
var tomlConfigTemplate = `[client]
allowed_origins = []
[client.token]
hmac_secret_key = "{{.TokenSecret}}"
[admin]
enabled = false
password = "{{.AdminPassword}}"
secret = "{{.AdminSecret}}"
[http_api]
key = "{{.APIKey}}"
`
var yamlConfigTemplate = `client:
token:
hmac_secret_key: "{{.TokenSecret}}"
allowed_origins: []
admin:
enabled: false
password: "{{.AdminPassword}}"
secret: "{{.AdminSecret}}"
http_api:
key: "{{.APIKey}}"
`
// GenerateConfig generates configuration file at provided path.
func GenerateConfig(f string) error {
exists, err := PathExists(f)
if err != nil {
return err
}
if exists {
return errors.New("output config file already exists: " + f)
}
ext := filepath.Ext(f)
if len(ext) > 1 {
ext = ext[1:]
}
supportedExtensions := []string{"json", "toml", "yaml", "yml"}
var t *template.Template
switch ext {
case "json":
t, err = template.New("config").Parse(jsonConfigTemplate)
case "toml":
t, err = template.New("config").Parse(tomlConfigTemplate)
case "yaml", "yml":
t, err = template.New("config").Parse(yamlConfigTemplate)
default:
return errors.New("output config file must have one of supported extensions: " + strings.Join(supportedExtensions, ", "))
}
if err != nil {
return err
}
var output bytes.Buffer
_ = t.Execute(&output, struct {
TokenSecret string
AdminPassword string
AdminSecret string
APIKey string
}{
mustGenerateSecretKey(64),
mustGenerateSecretKey(16),
mustGenerateSecretKey(64),
mustGenerateSecretKey(64),
})
return os.WriteFile(f, output.Bytes(), 0600)
}
func OptionalStringChoice(value string, choices []string) (string, error) {
if value == "" {
// Empty value is valid for optional configuration key.
return value, nil
}
if !slices.Contains(choices, value) {
return "", fmt.Errorf("invalid value: %s, possible choices are: %s", value, strings.Join(choices, ", "))
}
return value, nil
}
func mustGenerateSecretKey(byteLen int) string {
key := make([]byte, byteLen)
if _, err := rand.Read(key); err != nil {
panic(err)
}
return base64.RawURLEncoding.EncodeToString(key)
}

15
internal/tools/file.go Normal file
View File

@@ -0,0 +1,15 @@
package tools
import (
"os"
)
func FileExists(filename string) bool {
_, err := os.Stat(filename)
if err != nil {
if os.IsNotExist(err) {
return false // File does not exist.
}
}
return true // File exists.
}

15
internal/tools/http.go Normal file
View File

@@ -0,0 +1,15 @@
package tools
// GetAcceptProtocolLabel returns the transport accept protocol label based on HTTP version.
func GetAcceptProtocolLabel(protoMajor int) string {
switch protoMajor {
case 3:
return "h3"
case 2:
return "h2"
case 1:
return "h1"
default:
return "unknown"
}
}

View File

@@ -0,0 +1,35 @@
package tools
import (
"sync/atomic"
"time"
)
// IntervalLimiter permits an action at most once per configured interval. It is
// safe for concurrent use and does no allocation or locking on the hot path -
// just an atomic load (and a single CAS when the action is actually allowed),
// so guarding a rare log line with it is effectively free when the interval has
// not elapsed.
type IntervalLimiter struct {
intervalNanos int64
lastNanos atomic.Int64
}
// NewIntervalLimiter creates an IntervalLimiter that allows an action at most
// once per interval.
func NewIntervalLimiter(interval time.Duration) *IntervalLimiter {
return &IntervalLimiter{intervalNanos: int64(interval)}
}
// Allow reports whether the action may run now. Across all concurrent callers it
// returns true at most once per interval. The first call always returns true.
func (l *IntervalLimiter) Allow() bool {
now := time.Now().UnixNano()
last := l.lastNanos.Load()
if now-last < l.intervalNanos {
return false
}
// Only the caller that swaps in the new timestamp is allowed to proceed, so
// concurrent callers in the same window can't all log.
return l.lastNanos.CompareAndSwap(last, now)
}

View File

@@ -0,0 +1,35 @@
package tools
import (
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestIntervalLimiterThrottles(t *testing.T) {
l := NewIntervalLimiter(time.Hour)
require.True(t, l.Allow(), "first call should be allowed")
require.False(t, l.Allow(), "second call within interval should be throttled")
require.False(t, l.Allow())
}
func TestIntervalLimiterConcurrentSingleAllow(t *testing.T) {
l := NewIntervalLimiter(time.Hour)
const n = 200
var allowed int64
var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
if l.Allow() {
atomic.AddInt64(&allowed, 1)
}
}()
}
wg.Wait()
require.Equal(t, int64(1), allowed, "exactly one concurrent caller should be allowed per interval")
}

30
internal/tools/json.go Normal file
View File

@@ -0,0 +1,30 @@
package tools
import "encoding/json"
// IsValidJSON checks if the given byte slice is valid JSON.
func IsValidJSON(data []byte) bool {
return json.Valid(data)
}
// IsValidJSONObject checks if the given byte slice is valid JSON and specifically a JSON object.
func IsValidJSONObject(data []byte) bool {
if !IsValidJSON(data) {
return false
}
// Find first non-whitespace character
start := 0
for start < len(data) && isWhitespace(data[start]) {
start++
}
// Find last non-whitespace character
end := len(data) - 1
for end >= start && isWhitespace(data[end]) {
end--
}
return start <= end && data[start] == '{' && data[end] == '}'
}
func isWhitespace(b byte) bool {
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
}

181
internal/tools/json_test.go Normal file
View File

@@ -0,0 +1,181 @@
package tools
import (
"testing"
)
func TestIsValidJSON(t *testing.T) {
tests := []struct {
name string
input []byte
expected bool
}{
{
name: "empty data",
input: []byte{},
expected: false,
},
{
name: "valid json object",
input: []byte(`{"key": "value"}`),
expected: true,
},
{
name: "valid json array",
input: []byte(`[1, 2, 3]`),
expected: true,
},
{
name: "valid json string",
input: []byte(`"hello"`),
expected: true,
},
{
name: "valid json number",
input: []byte(`123`),
expected: true,
},
{
name: "valid json boolean true",
input: []byte(`true`),
expected: true,
},
{
name: "valid json boolean false",
input: []byte(`false`),
expected: true,
},
{
name: "valid json null",
input: []byte(`null`),
expected: true,
},
{
name: "valid empty json object",
input: []byte(`{}`),
expected: true,
},
{
name: "valid empty json array",
input: []byte(`[]`),
expected: true,
},
{
name: "invalid json - missing closing brace",
input: []byte(`{"key": "value"`),
expected: false,
},
{
name: "invalid json - plain text",
input: []byte(`hello world`),
expected: false,
},
{
name: "invalid json - trailing comma",
input: []byte(`{"key": "value",}`),
expected: false,
},
{
name: "invalid json - single quote",
input: []byte(`{'key': 'value'}`),
expected: false,
},
{
name: "invalid json - unquoted key",
input: []byte(`{key: "value"}`),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsValidJSON(tt.input)
if got != tt.expected {
t.Errorf("IsValidJSON() = %v, want %v", got, tt.expected)
}
})
}
}
func TestIsValidJSONObject(t *testing.T) {
tests := []struct {
name string
input []byte
expected bool
}{
{
name: "empty data",
input: []byte{},
expected: false,
},
{
name: "valid json object",
input: []byte(`{"key": "value"}`),
expected: true,
},
{
name: "valid empty json object",
input: []byte(`{}`),
expected: true,
},
{
name: "valid json object with whitespace",
input: []byte(` {"key": "value"} `),
expected: true,
},
{
name: "valid json object with newlines",
input: []byte("{\n \"key\": \"value\"\n}"),
expected: true,
},
{
name: "valid json array - should be false",
input: []byte(`[1, 2, 3]`),
expected: false,
},
{
name: "valid json string - should be false",
input: []byte(`"hello"`),
expected: false,
},
{
name: "valid json number - should be false",
input: []byte(`123`),
expected: false,
},
{
name: "valid json boolean - should be false",
input: []byte(`true`),
expected: false,
},
{
name: "valid json null - should be false",
input: []byte(`null`),
expected: false,
},
{
name: "invalid json - missing closing brace",
input: []byte(`{"key": "value"`),
expected: false,
},
{
name: "invalid json - plain text",
input: []byte(`hello world`),
expected: false,
},
{
name: "invalid json - trailing comma",
input: []byte(`{"key": "value",}`),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsValidJSONObject(tt.input)
if got != tt.expected {
t.Errorf("IsValidJSONObject() = %v, want %v", got, tt.expected)
}
})
}
}

59
internal/tools/logging.go Normal file
View File

@@ -0,0 +1,59 @@
package tools
import (
"net/url"
"strings"
)
// StripPassword from URL address.
func StripPassword(address string) string {
u, err := url.Parse(address)
if err != nil {
return address
}
// Use url.Redacted, which replaces the password in the parsed userinfo before
// re-encoding. The previous approach string-replaced the DECODED password in
// the ENCODED URL, so a password containing a URL-special char (e.g. "@", a
// space, or non-ASCII) never matched and was logged in the clear.
return u.Redacted()
}
// GetLogAddresses returns a string with addresses (concatenated with comma)
// with password stripped from each address.
func GetLogAddresses(addresses []string) string {
cleanedAddresses := make([]string, 0, len(addresses))
for _, a := range addresses {
cleanedAddress := StripPassword(a)
cleanedAddresses = append(cleanedAddresses, cleanedAddress)
}
return strings.Join(cleanedAddresses, ", ")
}
// RedactedLogURLs prepares URLs to be logged or shown in UI stripping auth info from them.
func RedactedLogURLs(urls ...string) []string {
var result []string
for _, input := range urls {
// Split the input by commas to handle comma-separated URLs.
urlParts := strings.Split(input, ",")
var cleanedParts []string
for _, urlString := range urlParts {
parsedURL, err := url.Parse(strings.TrimSpace(urlString))
var cleanedURL string
if err != nil {
cleanedURL = "<invalid_url>"
} else {
cleanedURL = parsedURL.Redacted()
}
cleanedParts = append(cleanedParts, cleanedURL)
}
// Combine the cleaned URLs back into a comma-separated string.
if len(cleanedParts) > 0 {
result = append(result, strings.Join(cleanedParts, ","))
}
}
return result
}

View File

@@ -0,0 +1,67 @@
package tools
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestGetLogURLs tests the RedactedLogURLs function using Redacted method.
func TestGetLogURLs(t *testing.T) {
t.Run("Single URL with auth info", func(t *testing.T) {
input := "https://user:password@domain.com/resource"
expected := []string{"https://user:xxxxx@domain.com/resource"}
actual := RedactedLogURLs(input)
require.Equal(t, expected, actual)
})
t.Run("Multiple URLs with mixed auth info", func(t *testing.T) {
input := "https://user:pass@domain.com/resource,https://another.com"
expected := []string{"https://user:xxxxx@domain.com/resource,https://another.com"}
actual := RedactedLogURLs(input)
require.Equal(t, expected, actual)
})
t.Run("Multiple URLs with mixed spaces", func(t *testing.T) {
input := "https://user:pass@domain.com/resource, https://another.com"
expected := []string{"https://user:xxxxx@domain.com/resource,https://another.com"}
actual := RedactedLogURLs(input)
require.Equal(t, expected, actual)
})
t.Run("Single URL without auth info", func(t *testing.T) {
input := "https://domain.com/resource"
expected := []string{"https://domain.com/resource"}
actual := RedactedLogURLs(input)
require.Equal(t, expected, actual)
})
t.Run("Invalid URL", func(t *testing.T) {
input := "://invalid-url"
expected := []string{"<invalid_url>"}
actual := RedactedLogURLs(input)
require.Equal(t, expected, actual)
})
t.Run("Mixed valid and invalid URLs", func(t *testing.T) {
input := "https://user:pass@domain.com/resource, ://invalid-url, https://valid.com"
expected := []string{"https://user:xxxxx@domain.com/resource,<invalid_url>,https://valid.com"}
actual := RedactedLogURLs(input)
require.Equal(t, expected, actual)
})
t.Run("Multiple comma-separated URLs with auth", func(t *testing.T) {
input := "https://user:pass@domain.com, https://admin:admin@another.com, httpss://example.com/resource"
expected := []string{"https://user:xxxxx@domain.com,https://admin:xxxxx@another.com,httpss://example.com/resource"}
actual := RedactedLogURLs(input)
require.Equal(t, expected, actual)
})
t.Run("GRPC addresses work correctly", func(t *testing.T) {
// We use such format for GRPC proxy config.
input := []string{"grpc://user:pass@127.0.0.1:9000", "grpc://127.0.0.1:10000"}
expected := []string{"grpc://user:xxxxx@127.0.0.1:9000", "grpc://127.0.0.1:10000"}
actual := RedactedLogURLs(input...)
require.Equal(t, expected, actual)
})
}

14
internal/tools/pid.go Normal file
View File

@@ -0,0 +1,14 @@
package tools
import (
"os"
"strconv"
)
func WritePidFile(pidFile string) error {
if pidFile == "" {
return nil
}
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
return os.WriteFile(pidFile, pid, 0600)
}

View File

@@ -0,0 +1,287 @@
package tools
import (
"context"
"errors"
"io"
"log"
"net/http"
"net/http/httptest"
"sync"
"github.com/centrifugal/centrifugo/v6/internal/proxyproto"
"github.com/centrifugal/centrifuge"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
)
// TestTransport - test transport
type TestTransport struct {
mu sync.Mutex
sink chan []byte
closed bool
closeCh chan struct{}
disconnect *centrifuge.Disconnect
protoType centrifuge.ProtocolType
}
// NewTestTransport - builder for TestTransport
func NewTestTransport() *TestTransport {
return &TestTransport{
protoType: centrifuge.ProtocolTypeJSON,
closeCh: make(chan struct{}),
}
}
// Write - ...
func (t *TestTransport) Write(message []byte) error {
return t.WriteMany(message)
}
// WriteMany - ...
func (t *TestTransport) WriteMany(messages ...[]byte) error {
t.mu.Lock()
defer t.mu.Unlock()
if t.closed {
return io.EOF
}
for _, buf := range messages {
dataCopy := make([]byte, len(buf))
copy(dataCopy, buf)
if t.sink != nil {
t.sink <- dataCopy
}
}
return nil
}
// Name - ...
func (t *TestTransport) Name() string {
return "test_transport"
}
func (t *TestTransport) AcceptProtocol() string {
return "h1"
}
// Protocol - ...
func (t *TestTransport) Protocol() centrifuge.ProtocolType {
return t.protoType
}
// ProtocolVersion returns transport protocol version.
func (t *TestTransport) ProtocolVersion() centrifuge.ProtocolVersion {
return centrifuge.ProtocolVersion2
}
// Unidirectional - ...
func (t *TestTransport) Unidirectional() bool {
return false
}
// DisabledPushFlags - ...
func (t *TestTransport) DisabledPushFlags() uint64 {
return centrifuge.PushFlagDisconnect
}
// PingPongConfig ...
func (t *TestTransport) PingPongConfig() centrifuge.PingPongConfig {
return centrifuge.PingPongConfig{}
}
// Emulation ...
func (t *TestTransport) Emulation() bool {
return false
}
// Close - ...
func (t *TestTransport) Close(disconnect centrifuge.Disconnect) error {
t.mu.Lock()
defer t.mu.Unlock()
if t.closed {
return nil
}
t.disconnect = &disconnect
t.closed = true
close(t.closeCh)
return nil
}
// NodeWithMemoryEngineNoHandlers - builder for centrifuge node with memory engine
func NodeWithMemoryEngineNoHandlers() *centrifuge.Node {
n, err := centrifuge.New(centrifuge.Config{})
if err != nil {
panic(err)
}
err = n.Run()
if err != nil {
panic(err)
}
return n
}
// NodeWithMemoryEngine - builder for centrifuge node with memory engine
func NodeWithMemoryEngine() *centrifuge.Node {
n := NodeWithMemoryEngineNoHandlers()
n.OnConnect(func(client *centrifuge.Client) {
client.OnSubscribe(func(_ centrifuge.SubscribeEvent, cb centrifuge.SubscribeCallback) {
cb(centrifuge.SubscribeReply{}, nil)
})
client.OnPublish(func(_ centrifuge.PublishEvent, cb centrifuge.PublishCallback) {
cb(centrifuge.PublishReply{}, nil)
})
})
return n
}
type CommonHTTPProxyTestCase struct {
Node *centrifuge.Node
Client *centrifuge.Client
ClientCloseFunc centrifuge.ClientCloseFunc
Server *httptest.Server
Mux *http.ServeMux
}
func NewCommonHTTPProxyTestCase(ctx context.Context) *CommonHTTPProxyTestCase {
node := NodeWithMemoryEngineNoHandlers()
mux := http.NewServeMux()
server := httptest.NewServer(mux)
client, closeFn, err := centrifuge.NewClient(ctx, node, NewTestTransport())
if err != nil {
log.Fatalf("could not create centrifuge client: %v", err)
}
return &CommonHTTPProxyTestCase{
Node: node,
Client: client,
ClientCloseFunc: closeFn,
Server: server,
Mux: mux,
}
}
func (c *CommonHTTPProxyTestCase) Teardown() {
defer func() { _ = c.Node.Shutdown(context.Background()) }()
defer func() { _ = c.ClientCloseFunc() }()
c.Server.Close()
}
type CommonGRPCProxyTestCase struct {
Node *centrifuge.Node
Client *centrifuge.Client
ClientCloseFunc centrifuge.ClientCloseFunc
Server *grpc.Server
Listener *bufconn.Listener
}
func NewCommonGRPCProxyTestCase(ctx context.Context, srv proxyproto.CentrifugoProxyServer) *CommonGRPCProxyTestCase {
node := NodeWithMemoryEngineNoHandlers()
client, closeFn, err := centrifuge.NewClient(ctx, node, NewTestTransport())
if err != nil {
log.Fatalf("could not create centrifuge client: %v", err)
}
listener := bufconn.Listen(1024)
server := grpc.NewServer()
proxyproto.RegisterCentrifugoProxyServer(server, srv)
go func() {
if err := server.Serve(listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
log.Fatalf("GRPC server exited with error: %v", err)
}
}()
return &CommonGRPCProxyTestCase{
Node: node,
Client: client,
ClientCloseFunc: closeFn,
Server: server,
Listener: listener,
}
}
func (c *CommonGRPCProxyTestCase) Teardown() {
defer func() { _ = c.Node.Shutdown(context.Background()) }()
defer func() { _ = c.ClientCloseFunc() }()
c.Server.Stop()
}
type TestClientMock struct {
IDFunc func() string
UserIDFunc func() string
IsSubscribedFunc func(string) bool
ContextFunc func() context.Context
TransportFunc func() centrifuge.TransportInfo
storageMu sync.Mutex
storage map[string]any
}
func (m *TestClientMock) Send(bytes []byte) error {
//TODO implement me
panic("implement me")
}
func (m *TestClientMock) Disconnect(disconnect ...centrifuge.Disconnect) {
//TODO implement me
panic("implement me")
}
func (m *TestClientMock) WritePublication(channel string, publication *centrifuge.Publication, sp centrifuge.StreamPosition) error {
//TODO implement me
panic("implement me")
}
func (m *TestClientMock) ID() string {
if m.IDFunc != nil {
return m.IDFunc()
}
panic("not implemented")
}
func (m *TestClientMock) UserID() string {
if m.UserIDFunc != nil {
return m.UserIDFunc()
}
panic("not implemented")
}
func (m *TestClientMock) IsSubscribed(s string) bool {
if m.IsSubscribedFunc != nil {
return m.IsSubscribedFunc(s)
}
panic("not implemented")
}
func (m *TestClientMock) Context() context.Context {
if m.ContextFunc != nil {
return m.ContextFunc()
}
panic("not implemented")
}
func (m *TestClientMock) Transport() centrifuge.TransportInfo {
if m.TransportFunc != nil {
return m.TransportFunc()
}
panic("not implemented")
}
func (m *TestClientMock) AcquireStorage() (map[string]any, func(map[string]any)) {
m.storageMu.Lock()
if m.storage == nil {
m.storage = map[string]any{}
}
return m.storage, func(updatedStorage map[string]any) {
m.storage = updatedStorage
m.storageMu.Unlock()
}
}
func (m *TestClientMock) Unsubscribe(ch string, unsubscribe ...centrifuge.Unsubscribe) {
//TODO implement me
panic("implement me")
}