Skip to content
This repository was archived by the owner on Aug 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
root = true

[*]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4

[*.cs]
# --- Language style ---
csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, file, required:suggestion
csharp_space_after_cast = true
csharp_new_line_before_members_in_object_initializers = false
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion

dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_qualification_for_field = true:suggestion
dotnet_style_qualification_for_property = true:suggestion
dotnet_style_qualification_for_method = true:suggestion
dotnet_style_qualification_for_event = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion

# --- Expression-bodied members: use an expression body when the member is a single statement ---
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion
csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = when_on_single_line:suggestion
csharp_style_expression_bodied_indexers = when_on_single_line:suggestion
csharp_style_expression_bodied_accessors = when_on_single_line:suggestion

# --- Naming (warning-level) ---
# private instance / static fields: camelCase, NO underscore prefix (CONVENTIONS §4)
dotnet_naming_rule.private_fields_camel_case.severity = warning
dotnet_naming_rule.private_fields_camel_case.symbols = private_fields
dotnet_naming_rule.private_fields_camel_case.style = camel_case
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private

# private const: PascalCase (declared before the general rule so it wins)
dotnet_naming_rule.private_const_pascal_case.severity = warning
dotnet_naming_rule.private_const_pascal_case.symbols = private_const_fields
dotnet_naming_rule.private_const_pascal_case.style = pascal_case
dotnet_naming_symbols.private_const_fields.applicable_kinds = field
dotnet_naming_symbols.private_const_fields.applicable_accessibilities = private
dotnet_naming_symbols.private_const_fields.required_modifiers = const

# private static readonly: PascalCase
dotnet_naming_rule.private_static_readonly_pascal_case.severity = warning
dotnet_naming_rule.private_static_readonly_pascal_case.symbols = private_static_readonly_fields
dotnet_naming_rule.private_static_readonly_pascal_case.style = pascal_case
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = static, readonly

dotnet_naming_style.camel_case.capitalization = camel_case
dotnet_naming_style.pascal_case.capitalization = pascal_case

# --- ReSharper / Rider hints (IDE-only; not enforced by dotnet build) ---
# Collapse blank lines in method bodies to 0 — backs the "no blank lines between arrange/act/assert"
# rule. Rider-enforced; CI cannot enforce this via editorconfig alone.
resharper_csharp_keep_blank_lines_in_code = 0
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_braces_for_ifelse = required
resharper_braces_for_for = required
resharper_braces_for_foreach = required
resharper_braces_for_while = required
resharper_trailing_comma_in_multiline_lists = true

[*.{csproj,props,targets,nuspec}]
indent_size = 2

[*.{json,jsonc}]
indent_size = 2

[*.{yaml,yml}]
indent_size = 2

[*.{bash,sh,zsh}]
indent_size = 2
51 changes: 51 additions & 0 deletions RealtimeTests/Broadcast/BroadcastMessageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RealtimeTests.Models;
using RealtimeTests.Support;
using Supabase.Realtime.Socket;

namespace RealtimeTests.Broadcast;

/// <summary>
/// What a channel does with an inbound <c>broadcast</c> frame: it routes it to the registered broadcast
/// instance, which types the payload and hands it to listeners. Driven through the channel's public
/// message entry point over a fake socket, so no live server is needed.
/// </summary>
[TestClass]
[TestCategory("Unit")]
public class BroadcastMessageTests
{
private const string Frame =
"{\"topic\":\"realtime:example\",\"event\":\"broadcast\",\"ref\":\"srv-1\",\"payload\":{\"event\":\"user\",\"userId\":\"abc-123\"}}";

[TestMethod]
public void HandleSocketMessage_ShouldDeliverTypedBroadcastToListener()
{
var channel = Wire.Channel();
var broadcast = channel.Register<BroadcastExample>();
BroadcastExample? received = null;
broadcast.AddBroadcastEventHandler((_, _) => received = broadcast.Current());
channel.HandleSocketMessage(Wire.Decode(Frame));
received.Should().NotBeNull();
received!.UserId.Should().Be("abc-123");
received.Event.Should().Be("user");
}

[TestMethod]
public void Current_ShouldReturnNull_GivenNoBroadcastReceived()
{
var channel = Wire.Channel();
var broadcast = channel.Register<BroadcastExample>();
broadcast.Current().Should().BeNull();
}

[TestMethod]
public void TriggerReceived_ShouldThrow_GivenUnparsableResponse()
{
var channel = Wire.Channel();
var broadcast = channel.Register<BroadcastExample>();
var act = () => broadcast.TriggerReceived(new SocketResponse(Wire.Settings()));
act.Should().Throw<ArgumentException>();
}
}
46 changes: 46 additions & 0 deletions RealtimeTests/Broadcast/BroadcastRegistrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RealtimeTests.Models;
using RealtimeTests.Support;
using Supabase.Realtime.Broadcast;

namespace RealtimeTests.Broadcast;

/// <summary>
/// Client-side rules a channel enforces when registering for broadcast, before any server is involved:
/// broadcast can only be registered once, and replay-from-history requires a private channel.
/// </summary>
[TestClass]
[TestCategory("Unit")]
public class BroadcastRegistrationTests
{
private static BroadcastOptions WithReplay() => new()
{
Replay = new BroadcastOptions.ReplayOptions { Limit = 10, Since = 0 }
};

[TestMethod]
public void Register_ShouldThrow_GivenReplayOnPublicChannel()
{
var channel = Wire.Channel(options: Wire.PublicOptions());
var act = () => channel.Register<BroadcastExample>(WithReplay());
act.Should().Throw<InvalidOperationException>();
}

[TestMethod]
public void Register_ShouldSucceed_GivenReplayOnPrivateChannel()
{
var channel = Wire.Channel(options: Wire.PrivateOptions());
channel.Register<BroadcastExample>(WithReplay()).Should().NotBeNull();
}

[TestMethod]
public void Register_ShouldThrow_GivenCalledTwice()
{
var channel = Wire.Channel();
channel.Register<BroadcastExample>();
var act = () => channel.Register<BroadcastExample>();
act.Should().Throw<InvalidOperationException>();
}
}
155 changes: 155 additions & 0 deletions RealtimeTests/Broadcast/BroadcastRelayTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using RealtimeTests.Models;
using Supabase.Postgrest.Interfaces;
using Supabase.Realtime;
using Supabase.Realtime.Broadcast;
using Supabase.Realtime.Channel;
using Supabase.Realtime.Interfaces;

namespace RealtimeTests.Broadcast;

/// <summary>
/// Broadcast against the live stack: messages relay between two clients, work on private (RLS-authorized)
/// channels, replay from history on a private channel, and a send resolves even when no server ack was
/// configured (supabase-community/realtime-csharp#38).
/// </summary>
[TestClass]
[TestCategory("E2E")]
public class BroadcastRelayTests
{
private IPostgrestClient restClient = null!;
private IRealtimeClient<RealtimeSocket, RealtimeChannel> socketClient = null!;

[TestInitialize]
public async Task InitializeTest()
{
restClient = Helpers.RestClient();
socketClient = Helpers.SocketClient();
await socketClient.ConnectAsync();
}

[TestCleanup]
public void CleanupTest() => socketClient.Disconnect();

[TestMethod]
public async Task Broadcast_ShouldRelayBetweenClients()
{
var tsc = new TaskCompletionSource<bool>();
var tsc2 = new TaskCompletionSource<bool>();
var guid1 = Guid.NewGuid().ToString();
var guid2 = Guid.NewGuid().ToString();
var channel1 = socketClient.Channel("online-users");
var broadcast1 = channel1.Register<BroadcastExample>(true, true);
broadcast1.AddBroadcastEventHandler((_, _) =>
{
var broadcast = broadcast1.Current();
if (broadcast?.UserId != guid1 && broadcast?.Event == "user")
tsc.TrySetResult(true);
});
var client2 = Helpers.SocketClient();
await client2.ConnectAsync();
var channel2 = client2.Channel("online-users");
var broadcast2 = channel2.Register<BroadcastExample>(true, true);
broadcast2.AddBroadcastEventHandler((_, _) =>
{
var broadcast = broadcast2.Current();
if (broadcast?.UserId != guid2 && broadcast?.Event == "user")
tsc2.TrySetResult(true);
});
await channel1.Subscribe();
await channel2.Subscribe();
await broadcast1.Send("user", new BroadcastExample { UserId = guid1 });
await broadcast2.Send("user", new BroadcastExample { UserId = guid2 });
await Task.WhenAll(tsc.Task, tsc2.Task);
}

[TestMethod]
public async Task Broadcast_ShouldRelayOnPrivateChannel()
{
var tsc = new TaskCompletionSource<bool>();
var tsc2 = new TaskCompletionSource<bool>();
var guid1 = Guid.NewGuid().ToString();
var guid2 = Guid.NewGuid().ToString();
var client1 = Helpers.PrivateSocketClient();
await client1.ConnectAsync();
var channel1 = client1.Channel("online-users",
ChannelOptions.Private(client1.Options, () => Helpers.ApiKey, new JsonSerializerSettings()));
var broadcast1 = channel1.Register<BroadcastExample>(true, true);
broadcast1.AddBroadcastEventHandler((_, _) =>
{
var broadcast = broadcast1.Current();
if (broadcast?.UserId != guid1 && broadcast?.Event == "user")
tsc.TrySetResult(true);
});
var client2 = Helpers.PrivateSocketClient();
await client2.ConnectAsync();
var channel2 = client2.Channel("online-users",
ChannelOptions.Private(client2.Options, () => Helpers.ApiKey, new JsonSerializerSettings()));
var broadcast2 = channel2.Register<BroadcastExample>(true, true);
broadcast2.AddBroadcastEventHandler((_, _) =>
{
var broadcast = broadcast2.Current();
if (broadcast?.UserId != guid2 && broadcast?.Event == "user")
tsc2.TrySetResult(true);
});
await channel1.Subscribe();
await channel2.Subscribe();
await broadcast1.Send("user", new BroadcastExample { UserId = guid1 });
await broadcast2.Send("user", new BroadcastExample { UserId = guid2 });
await Task.WhenAll(tsc.Task, tsc2.Task);
}

[TestMethod]
public async Task Send_ShouldResolve_GivenNoExplicitAck()
{
// Mirrors the most natural usage: Channel(name) -> Subscribe() -> Send(), with no
// Register<T>(broadcastAck: true). See supabase-community/realtime-csharp#38.
var channel = socketClient.Channel("no-ack-broadcast");
await channel.Subscribe();
var sendTask = channel.Send(Constants.ChannelEventName.Broadcast, "test_event",
new BroadcastExample { UserId = Guid.NewGuid().ToString() });
var winner = await Task.WhenAny(sendTask, Task.Delay(TimeSpan.FromSeconds(5)));
Assert.AreSame(sendTask, winner, "channel.Send() did not complete within 5s (issue #38).");
Assert.IsTrue(await sendTask);
}

[TestMethod]
public async Task Broadcast_ShouldReplayHistory_GivenPrivateChannel()
{
var send = new Dictionary<string, object>
{
{ "event", "user" },
{ "topic", "online-users" },
{ "private", true }
};
await restClient.Rpc("send", send);
var tsc = new TaskCompletionSource<bool>();
var client1 = Helpers.PrivateSocketClient();
await client1.ConnectAsync();
var broadcastOptions = new BroadcastOptions
{
BroadcastAck = true,
BroadcastSelf = true,
Replay = new BroadcastOptions.ReplayOptions
{
Limit = 10,
Since = DateTimeOffset.UtcNow.AddDays(-3).ToUnixTimeMilliseconds()
}
};
var channel1 = client1.Channel("online-users",
ChannelOptions.Private(client1.Options, () => null, new JsonSerializerSettings()));
var broadcast1 = channel1.Register<BroadcastExample>(broadcastOptions);
broadcast1.AddBroadcastEventHandler((_, _) =>
{
var broadcast = broadcast1.Current();
if (broadcast is { Event: "user", Meta.Replayed: true })
tsc.TrySetResult(true);
});
await channel1.Subscribe();
await tsc.Task;
}
}
56 changes: 0 additions & 56 deletions RealtimeTests/ChannelBroadcastReplayTests.cs

This file was deleted.

Loading
Loading