From c7c2da269fe394d54cb15fdc18d3e8a544c433f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= Date: Fri, 31 Jan 2025 16:28:01 +0100 Subject: [PATCH 1/3] Add User Permissions code examples + Add related missing properties + Add unit tests (#154) * Add option to read & write app scoped permissions (grants) * fix naming * Add code examples related to user permissions + Change AddMembersAsync to return UpdateChannel + Add Members to UpdateChannelResponse + Add Grants to ChannelConfigBase + add ChannelRole to ChannelMember + add unit tests guaranteeing that the code examples work as stated * fix auto review errors * Allow settings grants explicitly to NULL. This is a special case where API will reset grants to default settings * Change test to create temporary channel types only (mac count is restricted by the API) --- samples/DocsExamples/UserPermissions.cs | 149 ++++++++++++++++++ src/Clients/ChannelClient.Members.cs | 6 +- src/Clients/IChannelClient.cs | 4 +- src/Models/Channel.cs | 1 + src/Models/ChannelConfig.cs | 1 + src/Models/ChannelMember.cs | 1 + src/Models/ChannelType.cs | 7 +- tests/BlocklistClientTests.cs | 8 +- tests/ChannelTypeClientTests.cs | 2 +- tests/CommandClientTests.cs | 4 +- tests/ImportClientTests.cs | 1 - tests/PermissionTests.cs | 198 +++++++++++++++++++++++- tests/TestBase.cs | 53 ++++++- tests/UserRolesTests.cs | 56 +++++++ 14 files changed, 466 insertions(+), 25 deletions(-) create mode 100644 samples/DocsExamples/UserPermissions.cs create mode 100644 tests/UserRolesTests.cs diff --git a/samples/DocsExamples/UserPermissions.cs b/samples/DocsExamples/UserPermissions.cs new file mode 100644 index 0000000..233ec16 --- /dev/null +++ b/samples/DocsExamples/UserPermissions.cs @@ -0,0 +1,149 @@ +using StreamChat.Clients; +using StreamChat.Models; + +namespace DocsExamples; + +/// +/// Code examples for +/// +internal class UserPermissions +{ + private readonly IUserClient _userClient; + private readonly IChannelClient _channelClient; + private readonly IPermissionClient _permissionClient; + private readonly IChannelTypeClient _channelTypeClient; + private readonly IAppClient _appClient; + + public UserPermissions() + { + var factory = new StreamClientFactory("{{ api_key }}", "{{ api_secret }}"); + _userClient = factory.GetUserClient(); + _channelClient = factory.GetChannelClient(); + _permissionClient = factory.GetPermissionClient(); + _channelTypeClient = factory.GetChannelTypeClient(); + _appClient = factory.GetAppClient(); + } + + internal async Task ChangeUserRole() + { + var upsertResponse = await _userClient.UpdatePartialAsync(new UserPartialRequest + { + Id = "user-id", + Set = new Dictionary + { + { "role", "special_agent" } + } + }); + } + + internal async Task VerifyChannelMemberRoleAssigned() + { + var addMembersResponse + = await _channelClient.AddMembersAsync("channel-type", "channel-id", new[] { "user-id" }); + Console.WriteLine(addMembersResponse.Members[0].ChannelRole); // channel role is equal to "channel_member" + } + + internal async Task AssignRoles() + { + // User must be a member of the channel before you can assign channel role + var resp = await _channelClient.AssignRolesAsync("channel-type", "channel-id", new AssignRoleRequest + { + AssignRoles = new List + { + new RoleAssignment { UserId = "user-id", ChannelRole = Role.ChannelModerator } + } + }); + } + + internal async Task CreateRole() + { + await _permissionClient.CreateRoleAsync("special_agent"); + } + + internal async Task DeleteRole() + { + await _permissionClient.DeleteRoleAsync("special_agent"); + } + + internal async Task ListPermissions() + { + var response = await _permissionClient.ListPermissionsAsync(); + } + + internal async Task UpdateGrantedPermissions() + { + // observe current grants of the channel type + var channelType = await _channelTypeClient.GetChannelTypeAsync("messaging"); + Console.WriteLine(channelType.Grants); + + // update "channel_member" role grants in "messaging" scope + var update = new ChannelTypeWithStringCommandsRequest + { + Grants = new Dictionary> + { + { + // This will replace all existing grants of "channel_member" role + "channel_member", new List + { + "read-channel", // allow access to the channel + "create-message", // create messages in the channel + "update-message-owner", // update own user messages + "delete-message-owner", // delete own user messages + } + }, + } + }; + await _channelTypeClient.UpdateChannelTypeAsync("messaging", update); + } + + internal async Task RemoveGrantedPermissionsByCategory() + { + var update = new ChannelTypeWithStringCommandsRequest + { + Grants = new Dictionary> + { + { "guest", new List() }, // removes all grants of "guest" role + { "anonymous", new List() }, // removes all grants of "anonymous" role + } + }; + await _channelTypeClient.UpdateChannelTypeAsync("messaging", update); + } + + internal async Task ResetGrantsToDefaultSettings() + { + var update = new ChannelTypeWithStringCommandsRequest + { + Grants = new Dictionary>() + }; + await _channelTypeClient.UpdateChannelTypeAsync("messaging", update); + } + + internal async Task UpdateAppScopedGrants() + { + var settings = new AppSettingsRequest + { + Grants = new Dictionary> + { + { "anonymous", new List() }, + { "guest", new List() }, + { "user", new List { "search-user", "mute-user" } }, + { "admin", new List { "search-user", "mute-user", "ban-user" } }, + } + }; + await _appClient.UpdateAppSettingsAsync(settings); + } + + internal async Task UpdateChannelLevelPermissions() + { + var grants = new Dictionary { { "user", new List { "!add-links", "create-reaction" } } }; + var overrides = new Dictionary { { "grants", grants } }; + var request = new PartialUpdateChannelRequest + { + Set = new Dictionary + { + { "config_overrides", overrides } + } + }; + var resp = await _channelClient.PartialUpdateAsync("channel-type", "channel-id", request); + } +} \ No newline at end of file diff --git a/src/Clients/ChannelClient.Members.cs b/src/Clients/ChannelClient.Members.cs index e7f5504..fcc9a01 100644 --- a/src/Clients/ChannelClient.Members.cs +++ b/src/Clients/ChannelClient.Members.cs @@ -8,11 +8,11 @@ namespace StreamChat.Clients { public partial class ChannelClient { - public async Task AddMembersAsync(string channelType, string channelId, params string[] userIds) + public async Task AddMembersAsync(string channelType, string channelId, params string[] userIds) => await AddMembersAsync(channelType, channelId, userIds, null, null); - public async Task AddMembersAsync(string channelType, string channelId, IEnumerable userIds, MessageRequest msg, AddMemberOptions options) - => await ExecuteRequestAsync($"channels/{channelType}/{channelId}", + public async Task AddMembersAsync(string channelType, string channelId, IEnumerable userIds, MessageRequest msg, AddMemberOptions options) + => await ExecuteRequestAsync($"channels/{channelType}/{channelId}", HttpMethod.POST, HttpStatusCode.Created, new ChannelUpdateRequest diff --git a/src/Clients/IChannelClient.cs b/src/Clients/IChannelClient.cs index 021dc5b..eeb55b8 100644 --- a/src/Clients/IChannelClient.cs +++ b/src/Clients/IChannelClient.cs @@ -14,13 +14,13 @@ public interface IChannelClient /// Adds members to a channel. /// /// https://getstream.io/chat/docs/dotnet-csharp/channel_members/?language=csharp - Task AddMembersAsync(string channelType, string channelId, params string[] userIds); + Task AddMembersAsync(string channelType, string channelId, params string[] userIds); /// /// Adds members to a channel. /// /// https://getstream.io/chat/docs/dotnet-csharp/channel_members/?language=csharp - Task AddMembersAsync(string channelType, string channelId, IEnumerable userIds, + Task AddMembersAsync(string channelType, string channelId, IEnumerable userIds, MessageRequest msg, AddMemberOptions options); /// diff --git a/src/Models/Channel.cs b/src/Models/Channel.cs index 1f01d7f..a890afb 100644 --- a/src/Models/Channel.cs +++ b/src/Models/Channel.cs @@ -51,6 +51,7 @@ public class UpdateChannelResponse : ApiResponse { public ChannelWithConfig Channel { get; set; } public Message Message { get; set; } + public List Members { get; set; } } public class PartialUpdateChannelRequest diff --git a/src/Models/ChannelConfig.cs b/src/Models/ChannelConfig.cs index 3fe85dd..ffceb1b 100644 --- a/src/Models/ChannelConfig.cs +++ b/src/Models/ChannelConfig.cs @@ -18,6 +18,7 @@ public abstract class ChannelConfigBase public string MessageRetention { get; set; } public int MaxMessageLength { get; set; } public string Automod { get; set; } + public Dictionary> Grants { get; set; } } public class ChannelConfig : ChannelConfigBase diff --git a/src/Models/ChannelMember.cs b/src/Models/ChannelMember.cs index 8bfb8ac..8aeb43e 100644 --- a/src/Models/ChannelMember.cs +++ b/src/Models/ChannelMember.cs @@ -19,6 +19,7 @@ public class ChannelMember : CustomDataBase public DateTimeOffset? InviteAcceptedAt { get; set; } public DateTimeOffset? InviteRejectedAt { get; set; } public string Role { get; set; } + public string ChannelRole { get; set; } public DateTimeOffset? CreatedAt { get; set; } public DateTimeOffset? UpdatedAt { get; set; } public bool? Banned { get; set; } diff --git a/src/Models/ChannelType.cs b/src/Models/ChannelType.cs index e94c752..d95f78a 100644 --- a/src/Models/ChannelType.cs +++ b/src/Models/ChannelType.cs @@ -62,7 +62,12 @@ public abstract class ChannelTypeRequestBase [Obsolete("Use V2 Permissions APIs instead. " + "See https://getstream.io/chat/docs/dotnet-csharp/migrating_from_legacy/?language=csharp")] public List Permissions { get; set; } - public Dictionary> Grants { get; set; } + + // JsonProperty is needed because passing NULL is a special case where API resets the grants to the default settings. + // Empty Dictionary as a default value is needed in order for the default object to not reset the grants + [JsonProperty(NullValueHandling = NullValueHandling.Include, + DefaultValueHandling = DefaultValueHandling.Include)] + public Dictionary> Grants { get; set; } = new Dictionary>(); } public class ChannelTypeWithCommandsRequest : ChannelTypeRequestBase diff --git a/tests/BlocklistClientTests.cs b/tests/BlocklistClientTests.cs index 0391004..90f380f 100644 --- a/tests/BlocklistClientTests.cs +++ b/tests/BlocklistClientTests.cs @@ -36,7 +36,7 @@ public async Task TearDownAsync() } [Test] - public Task TestGetAsync() => TryMultiple(async () => + public Task TestGetAsync() => TryMultipleAsync(async () => { var resp = await _blocklistClient.GetAsync(_blocklistName); @@ -47,7 +47,7 @@ public Task TestGetAsync() => TryMultiple(async () => }); [Test] - public Task TestListAsync() => TryMultiple(async () => + public Task TestListAsync() => TryMultipleAsync(async () => { var resp = await _blocklistClient.ListAsync(); @@ -59,12 +59,12 @@ public async Task TestUpdateAsync() { var expectedWords = new[] { "test", "test2" }; - await TryMultiple(async () => + await TryMultipleAsync(async () => { await _blocklistClient.UpdateAsync(_blocklistName, expectedWords); }); - await TryMultiple(async () => + await TryMultipleAsync(async () => { var updated = await _blocklistClient.GetAsync(_blocklistName); updated.Blocklist.Words.Should().BeEquivalentTo(expectedWords); diff --git a/tests/ChannelTypeClientTests.cs b/tests/ChannelTypeClientTests.cs index 4f94b45..5cd6960 100644 --- a/tests/ChannelTypeClientTests.cs +++ b/tests/ChannelTypeClientTests.cs @@ -52,7 +52,7 @@ await WaitForAsync(async () => [Test] public Task TestGetChannelTypeAsync() - => TryMultiple(testBody: async () => + => TryMultipleAsync(testBody: async () => { var actualChannelType = await _channelTypeClient.GetChannelTypeAsync(_channelType.Name); actualChannelType.Name.Should().BeEquivalentTo(_channelType.Name); diff --git a/tests/CommandClientTests.cs b/tests/CommandClientTests.cs index 0fc4d04..0ee82ed 100644 --- a/tests/CommandClientTests.cs +++ b/tests/CommandClientTests.cs @@ -38,7 +38,7 @@ public async Task TeardownAsync() [Test] public Task TestGetCommandAsync() - => TryMultiple(async () => + => TryMultipleAsync(async () => { var command = await _commandClient.GetAsync(_command.Name); @@ -47,7 +47,7 @@ public Task TestGetCommandAsync() [Test] public Task TestListCommandsAsync() - => TryMultiple(async () => + => TryMultipleAsync(async () => { var resp = await _commandClient.ListAsync(); diff --git a/tests/ImportClientTests.cs b/tests/ImportClientTests.cs index 8e6bb51..e8183f9 100644 --- a/tests/ImportClientTests.cs +++ b/tests/ImportClientTests.cs @@ -1,7 +1,6 @@ using System; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; using System.Threading.Tasks; using FluentAssertions; using NUnit.Framework; diff --git a/tests/PermissionTests.cs b/tests/PermissionTests.cs index 10d7965..fd58384 100644 --- a/tests/PermissionTests.cs +++ b/tests/PermissionTests.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using FluentAssertions; using NUnit.Framework; -using StreamChat; +using StreamChat.Clients; using StreamChat.Exceptions; using StreamChat.Models; @@ -24,12 +24,27 @@ public class PermissionTests : TestBase { private const string TestPermissionDescription = "Test Permission"; + private UserRequest _user1; + private UserRequest _user2; + [OneTimeSetUp] [OneTimeTearDown] public async Task CleanupAsync() { await DeleteCustomRolesAsync(); - await DeleteCustomPermissonsAsync(); + await DeleteCustomPermissionsAsync(); + } + + [SetUp] + public async Task SetupAsync() + { + (_user1, _user2) = (await UpsertNewUserAsync(), await UpsertNewUserAsync()); + } + + [TearDown] + public async Task TeardownAsync() + { + await TryDeleteUsersAsync(_user1.Id, _user2.Id); } private async Task DeleteCustomRolesAsync() @@ -49,7 +64,7 @@ private async Task DeleteCustomRolesAsync() } } - private async Task DeleteCustomPermissonsAsync() + private async Task DeleteCustomPermissionsAsync() { var permResp = await _permissionClient.ListPermissionsAsync(); foreach (var perm in permResp.Permissions.Where(x => x.Description == TestPermissionDescription)) @@ -67,7 +82,7 @@ private async Task DeleteCustomPermissonsAsync() } [Test] - public async Task TestRolesEnd2endAsync() + public async Task TestRolesEnd2EndAsync() { // Test create var roleResp = await _permissionClient.CreateRoleAsync(Guid.NewGuid().ToString()); @@ -100,7 +115,7 @@ public async Task TestRolesEnd2endAsync() } [Test] - public async Task TestPermissionsEnd2endAsync() + public async Task TestPermissionsEnd2EndAsync() { var permission = new Permission { @@ -143,7 +158,7 @@ public async Task TestPermissionsEnd2endAsync() { if (ex.Message.Contains("not found")) { - // Unfortounatly, the backend is too slow to propagate the permission creation + // Unfortunately, the backend is too slow to propagate the permission creation // so this error message is expected. Facepalm. return; } @@ -151,5 +166,174 @@ public async Task TestPermissionsEnd2endAsync() throw; } } + + [Test] + public async Task WhenUpdatingChannelGrantsExpectChannelGrantsChangedAsync() + { + ChannelTypeWithStringCommandsResponse tempChannelType = null; + try + { + tempChannelType = await CreateChannelTypeAsync(); + + // Expect delete-message-owner to not be present by default + tempChannelType.Grants.First(g => g.Key == "channel_member").Value.Should() + .NotContain("delete-message-owner"); + + var update = new ChannelTypeWithStringCommandsRequest + { + Grants = new Dictionary> + { + { + "channel_member", new List + { + "delete-message-owner", + } + }, + }, + }; + await TryMultipleAsync(() => _channelTypeClient.UpdateChannelTypeAsync(tempChannelType.Name, update)); + + var getChannelType2 = await _channelTypeClient.GetChannelTypeAsync(tempChannelType.Name); + + // Expect delete-message-owner to not be present by default + var channelMemberGrants = getChannelType2.Grants.First(g => g.Key == "channel_member").Value; + channelMemberGrants.Should().HaveCount(1); + channelMemberGrants.Should().Contain("delete-message-owner"); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + finally + { + try + { + if (tempChannelType != null) + { + await _channelTypeClient.DeleteChannelTypeAsync(tempChannelType.Name); + } + } + catch (Exception) + { + // ignored + } + } + } + + [Test] + public async Task WhenUpdatingGrantsWithEmptyListExpectResetToDefaultAsync() + { + var tempChannelType = await CreateChannelTypeAsync(); + + var channelMemberInitialGrantsCounts + = tempChannelType.Grants.First(g => g.Key == "channel_member").Value.Count; + + // We expect more than 1 grant by default + channelMemberInitialGrantsCounts.Should().NotBe(1); + + // Wait for data propagation - channel type is sometimes not present immediately after creation + await TryMultipleAsync(async () => + { + await _channelTypeClient.GetChannelTypeAsync(tempChannelType.Name); + }); + + // Override channel_members grants to replace with a single grant + var updateGrants = new ChannelTypeWithStringCommandsRequest + { + Grants = new Dictionary> + { + { + "channel_member", new List + { + "delete-message-owner", + } + }, + }, + }; + + // Try multiple times because it may fail due to data propagation + await TryMultipleAsync(async () => + { + var updateChannelTypeResponse + = await _channelTypeClient.UpdateChannelTypeAsync(tempChannelType.Name, updateGrants); + + // Confirm a single grant is present + updateChannelTypeResponse.Grants.First(g => g.Key == "channel_member").Value.Should().HaveCount(1); + }); + + // Try multiple times because it may fail due to data propagation + await TryMultipleAsync(async () => + { + // Restore grants + var restoreGrantsRequest + = new ChannelTypeWithStringCommandsRequest + { + Grants = null, + }; + var restoreGrantsResponse + = await _channelTypeClient.UpdateChannelTypeAsync(tempChannelType.Name, restoreGrantsRequest); + + // Assert more than 1 grant is present + restoreGrantsResponse.Grants.First(g => g.Key == "channel_member").Value.Should().HaveCountGreaterThan(1); + }); + } + + [Test] + public async Task WhenAssigningAppScopedPermissionsExpectAppGrantsMatchingAsync() + { + var settings = new AppSettingsRequest + { + Grants = new Dictionary> + { + { "anonymous", new List() }, + { "guest", new List() }, + { "user", new List { "search-user", "mute-user" } }, + { "admin", new List { "search-user", "mute-user", "ban-user" } }, + }, + }; + await _appClient.UpdateAppSettingsAsync(settings); + + var getAppResponse = await _appClient.GetAppSettingsAsync(); + getAppResponse.App.Grants.Should().NotBeNull(); + getAppResponse.App.Grants["anonymous"].Should().BeEmpty(); + getAppResponse.App.Grants["guest"].Should().BeEmpty(); + getAppResponse.App.Grants["user"].Should().BeEquivalentTo(new[] { "search-user", "mute-user" }); + getAppResponse.App.Grants["admin"].Should() + .BeEquivalentTo(new[] { "search-user", "mute-user", "ban-user" }); + } + + [Test] + public async Task WhenUpdatingChannelConfigGrantsOverridesExpectGrantsOverridenAsync() + { + var channel = await CreateChannelAsync(createdByUserId: _user1.Id); + await _channelClient.AddMembersAsync(channel.Type, channel.Id, new[] { _user2.Id }); + + var request = new PartialUpdateChannelRequest + { + Set = new Dictionary + { + { + "config_overrides", new Dictionary + { + { + "grants", new Dictionary + { + { + "user", new List { "!add-links", "create-reaction" } + }, + } + }, + } + }, + }, + }; + var partialUpdateChannelResponse + = await _channelClient.PartialUpdateAsync(channel.Type, channel.Id, request); + + var channelResp = await _channelClient.GetOrCreateAsync(channel.Type, channel.Id, new ChannelGetRequest()); + channelResp.Channel.Config.Grants["user"].Should() + .BeEquivalentTo(new List { "!add-links", "create-reaction" }); + } } -} +} \ No newline at end of file diff --git a/tests/TestBase.cs b/tests/TestBase.cs index 78c0c5d..b62b457 100644 --- a/tests/TestBase.cs +++ b/tests/TestBase.cs @@ -26,6 +26,7 @@ public abstract class TestBase protected static readonly ITaskClient _taskClient = TestClientFactory.GetTaskClient(); private readonly List _testChannels = new List(); + private readonly List _testChannelTypes = new List(); [OneTimeTearDown] public async Task OneTimeTearDown() @@ -33,13 +34,36 @@ public async Task OneTimeTearDown() const int chunkSize = 50; var cids = _testChannels.Select(x => x.Cid).ToArray(); - for (int i = 0; i < cids.Length; i += chunkSize) + for (var i = 0; i < cids.Length; i += chunkSize) { var chunk = cids.Skip(i).Take(chunkSize).ToArray(); - var resp = await _channelClient.DeleteChannelsAsync(chunk, hardDelete: true); - await WaitUntilTaskSucceedsAsync(resp.TaskId); + try + { + var resp = await _channelClient.DeleteChannelsAsync(chunk, hardDelete: true); + await WaitUntilTaskSucceedsAsync(resp.TaskId); + } + catch (Exception e) + { + Console.WriteLine($"Exception thrown while deleting channels: {e.Message}. Channels to delete: {string.Join(", ", chunk)}"); + } + } + + _testChannels.Clear(); + + foreach (var channelType in _testChannelTypes) + { + try + { + await _channelTypeClient.DeleteChannelTypeAsync(channelType); + } + catch (Exception e) + { + Console.WriteLine($"Exception thrown while deleting channel type: {e.Message}. Channel type to delete: {channelType}"); + } } + + _testChannelTypes.Clear(); } protected async Task WaitForAsync(Func> condition, int timeout = 5000, int delay = 500) @@ -105,6 +129,27 @@ protected async Task TryDeleteChannelAsync(string cid) await WaitUntilTaskSucceedsAsync(resp.TaskId); } + protected async Task CreateChannelTypeAsync(string name = null, bool autoDelete = true) + { + if (string.IsNullOrEmpty(name)) + { + name = Guid.NewGuid().ToString(); + } + + var channelType = await _channelTypeClient.CreateChannelTypeAsync( + new ChannelTypeWithStringCommandsRequest + { + Name = name, + }); + + if (autoDelete) + { + _testChannelTypes.Add(channelType.Name); + } + + return channelType; + } + protected async Task TryDeleteUsersAsync(params string[] userIds) { try @@ -128,7 +173,7 @@ await _userClient.DeleteManyAsync( /// How many times to try /// delay between a failed try /// Throws ArgumentException If max attempts or timeout exceeds the limit - protected async Task TryMultiple(Func testBody, + protected async Task TryMultipleAsync(Func testBody, int attempts = 5, int attemptTimeoutMs = 500) { const int maxAttempts = 20; diff --git a/tests/UserRolesTests.cs b/tests/UserRolesTests.cs new file mode 100644 index 0000000..3974ec7 --- /dev/null +++ b/tests/UserRolesTests.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using NUnit.Framework; +using StreamChat.Models; + +namespace StreamChatTests; + +internal class UserRolesTests : TestBase +{ + private UserRequest _user1; + private UserRequest _user2; + + [SetUp] + public async Task SetupAsync() + { + (_user1, _user2) = (await UpsertNewUserAsync(), await UpsertNewUserAsync()); + } + + [TearDown] + public async Task TeardownAsync() + { + await TryDeleteUsersAsync(_user1.Id, _user2.Id); + } + + [Test] + public async Task WhenUserIsAddedToChannelExpectChannelMemberRoleAssignedAsync() + { + var channel = await CreateChannelAsync(createdByUserId: _user1.Id); + + var addMembersResponse = await _channelClient.AddMembersAsync(channel.Type, channel.Id, new[] { _user2.Id }); + addMembersResponse.Members.First(m => m.UserId == _user2.Id).ChannelRole.Should() + .BeEquivalentTo("channel_member"); + + var getChannel = await _channelClient.GetOrCreateAsync(channel.Type, channel.Id, new ChannelGetRequest()); + + getChannel.Members.First(m => m.UserId == _user2.Id).ChannelRole.Should().BeEquivalentTo("channel_member"); + } + + [Test] + public async Task WhenAssigningARoleExpectRoleAssignedAsync() + { + var channel = await CreateChannelAsync(createdByUserId: _user1.Id); + await _channelClient.AddMembersAsync(channel.Type, channel.Id, new[] { _user2.Id }); + + var resp = await _channelClient.AssignRolesAsync(channel.Type, channel.Id, new AssignRoleRequest + { + AssignRoles = new List + { + new RoleAssignment { UserId = _user2.Id, ChannelRole = Role.ChannelModerator }, + }, + }); + resp.Members.First().ChannelRole.Should().BeEquivalentTo("channel_moderator"); + } +} \ No newline at end of file From 3e4d64c1e61fe0942e94083c4126b9b8c46a98c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= Date: Fri, 7 Feb 2025 12:09:14 +0100 Subject: [PATCH 2/3] feat: Add support for `MemberLimit` and `MessageLimit` when querying channels. (#156) * feat: Add support for `MemberLimit` and `MessageLimit` when querying channels. Can be set in `QueryChannelsOptions` * Add seperate channel for tests asserting message limits -> another test was truncating the messages * Add async postfix to test names --- src/Models/QueryOptions.cs | 2 ++ tests/ChannelClientTests.cs | 72 +++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/Models/QueryOptions.cs b/src/Models/QueryOptions.cs index b52c64e..0c8f715 100644 --- a/src/Models/QueryOptions.cs +++ b/src/Models/QueryOptions.cs @@ -76,6 +76,8 @@ public class QueryChannelsOptions public int Offset { get; set; } = DefaultOffset; public int Limit { get; set; } = DefaultLimit; + public int MemberLimit { get; set; } + public int MessageLimit { get; set; } public bool Presence { get; set; } public bool State { get; set; } public bool Watch { get; set; } diff --git a/tests/ChannelClientTests.cs b/tests/ChannelClientTests.cs index 8b8281d..b3ae802 100644 --- a/tests/ChannelClientTests.cs +++ b/tests/ChannelClientTests.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; -using Newtonsoft.Json; using NUnit.Framework; using StreamChat.Models; @@ -21,6 +20,7 @@ public class ChannelClientTests : TestBase private UserRequest _user2; private UserRequest _user3; private ChannelWithConfig _channel; + private ChannelWithConfig _channel2; [OneTimeSetUp] public async Task SetupAsync() @@ -29,7 +29,13 @@ public async Task SetupAsync() await UpsertNewUserAsync()); _channel = await CreateChannelAsync(createdByUserId: _user1.Id, members: new[] { _user1.Id, _user2.Id, _user3.Id }); - await _messageClient.SendMessageAsync(_channel.Type, _channel.Id, _user1.Id, "text"); + _channel2 = await CreateChannelAsync(createdByUserId: _user1.Id, + members: new[] { _user1.Id, _user2.Id, _user3.Id }); + await _messageClient.SendMessageAsync(_channel2.Type, _channel2.Id, _user1.Id, "text"); + await _messageClient.SendMessageAsync(_channel2.Type, _channel2.Id, _user1.Id, "text 2"); + await _messageClient.SendMessageAsync(_channel2.Type, _channel2.Id, _user1.Id, "text 3"); + await _messageClient.SendMessageAsync(_channel2.Type, _channel2.Id, _user1.Id, "text 4"); + await _messageClient.SendMessageAsync(_channel2.Type, _channel2.Id, _user1.Id, "text 5"); } [OneTimeTearDown] @@ -812,5 +818,67 @@ public async Task TestUnarchiveChannelForMemberAsync() archivedChannels.Channels.Should().NotContain(c => c.Channel.Cid == channel.Cid); } + + [Test] + public async Task TestQueryChannelsWithoutMembersLimitAsync() + { + var queryChannelResponse = await _channelClient.QueryChannelsAsync(new QueryChannelsOptions + { + Filter = new Dictionary() + { + { "cid", _channel2.Cid }, + }, + UserId = _user1.Id, + }); + queryChannelResponse.Channels.Should().HaveCount(1); + queryChannelResponse.Channels.First().Members.Should().HaveCount(3); + } + + [Test] + public async Task TestQueryChannelsWithMembersLimitAsync() + { + var queryChannelResponse = await _channelClient.QueryChannelsAsync(new QueryChannelsOptions + { + Filter = new Dictionary() + { + { "cid", _channel2.Cid }, + }, + MemberLimit = 1, + UserId = _user1.Id, + }); + queryChannelResponse.Channels.Should().HaveCount(1); + queryChannelResponse.Channels.First().Members.Should().HaveCount(1); + } + + [Test] + public async Task TestQueryChannelsWithoutMessageLimitAsync() + { + var queryChannelResponse = await _channelClient.QueryChannelsAsync(new QueryChannelsOptions + { + Filter = new Dictionary() + { + { "cid", _channel2.Cid }, + }, + UserId = _user1.Id, + }); + queryChannelResponse.Channels.Should().HaveCount(1); + queryChannelResponse.Channels.First().Messages.Should().HaveCount(5); + } + + [Test] + public async Task TestQueryChannelsWithMessageLimitAsync() + { + var queryChannelResponse = await _channelClient.QueryChannelsAsync(new QueryChannelsOptions + { + Filter = new Dictionary() + { + { "cid", _channel2.Cid }, + }, + MessageLimit = 2, + UserId = _user1.Id, + }); + queryChannelResponse.Channels.Should().HaveCount(1); + queryChannelResponse.Channels.First().Messages.Should().HaveCount(2); + } } } \ No newline at end of file From 60719ea8c9a7a575f0336c4c55402b9af8489331 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:44:29 +0100 Subject: [PATCH 3/3] chore(release): 2.9.0 (#157) Co-authored-by: github-actions --- CHANGELOG.md | 7 +++++++ src/stream-chat-net.csproj | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 292e98f..c3ceb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.9.0](https://github.com/GetStream/stream-chat-net/compare/2.8.0...2.9.0) (2025-02-07) + + +### Features + +* Add support for `MemberLimit` and `MessageLimit` when querying channels. ([#156](https://github.com/GetStream/stream-chat-net/issues/156)) ([3e4d64c](https://github.com/GetStream/stream-chat-net/commit/3e4d64c1e61fe0942e94083c4126b9b8c46a98c4)) + ## [2.8.0](https://github.com/GetStream/stream-chat-net/compare/2.7.0...2.8.0) (2025-01-31) ### Features diff --git a/src/stream-chat-net.csproj b/src/stream-chat-net.csproj index acea0ed..ebba798 100644 --- a/src/stream-chat-net.csproj +++ b/src/stream-chat-net.csproj @@ -10,7 +10,7 @@ stream-chat-net StreamChat .NET Client for Stream Chat - 2.8.0 + 2.9.0 Stream.io Stream.io © $([System.DateTime]::UtcNow.ToString(yyyy)) Stream.io