diff --git a/NetCord.slnx b/NetCord.slnx index c3ba02e9..23417afd 100644 --- a/NetCord.slnx +++ b/NetCord.slnx @@ -84,6 +84,7 @@ + diff --git a/NetCord/Gateway/Invite.cs b/NetCord/Gateway/Invite.cs index 2a15b626..434e951c 100644 --- a/NetCord/Gateway/Invite.cs +++ b/NetCord/Gateway/Invite.cs @@ -50,6 +50,10 @@ public Invite(JsonModels.JsonInvite jsonModel, RestClient client) public int Uses => _jsonModel.Uses; + public DateTimeOffset? ExpiresAt => _jsonModel.ExpiresAt; + + public IReadOnlyList? RoleIds => _jsonModel.RoleIds; + ulong? IInvite.ChannelId => ChannelId; int? IInvite.MaxAge => MaxAge; diff --git a/NetCord/Gateway/JsonModels/JsonInvite.cs b/NetCord/Gateway/JsonModels/JsonInvite.cs index 6618f96c..16078e9b 100644 --- a/NetCord/Gateway/JsonModels/JsonInvite.cs +++ b/NetCord/Gateway/JsonModels/JsonInvite.cs @@ -44,4 +44,10 @@ public class JsonInvite [JsonPropertyName("uses")] public int Uses { get; set; } + + [JsonPropertyName("expires_at")] + public DateTimeOffset? ExpiresAt { get; set; } + + [JsonPropertyName("role_ids")] + public ulong[]? RoleIds { get; set; } } diff --git a/NetCord/IInvite.cs b/NetCord/IInvite.cs index f4503371..73126e05 100644 --- a/NetCord/IInvite.cs +++ b/NetCord/IInvite.cs @@ -15,4 +15,5 @@ public interface IInvite public bool? Temporary { get; } public int? Uses { get; } public DateTimeOffset? CreatedAt { get; } + public DateTimeOffset? ExpiresAt { get; } } diff --git a/NetCord/InviteFlags.cs b/NetCord/InviteFlags.cs new file mode 100644 index 00000000..7daefb83 --- /dev/null +++ b/NetCord/InviteFlags.cs @@ -0,0 +1,7 @@ +namespace NetCord; + +[Flags] +public enum InviteFlags +{ + IsGuestInvite = 1 << 0, +} diff --git a/NetCord/Rest/InviteProperties.cs b/NetCord/Rest/InviteProperties.cs index 96d5d5bb..801da1f7 100644 --- a/NetCord/Rest/InviteProperties.cs +++ b/NetCord/Rest/InviteProperties.cs @@ -3,7 +3,7 @@ namespace NetCord.Rest; [GenerateMethodsForProperties] -public partial class InviteProperties +public partial class InviteProperties : IHttpSerializable { [JsonPropertyName("max_age")] public int? MaxAge { get; set; } @@ -25,4 +25,26 @@ public partial class InviteProperties [JsonPropertyName("target_application_id")] public ulong? TargetApplicationId { get; set; } + + [JsonIgnore] + public InviteTargetUsersProperties? TargetUsers { get; set; } + + [JsonPropertyName("role_ids")] + public IEnumerable? RoleIds { get; set; } + + HttpContent IHttpSerializable.Serialize() => Serialize(); + + internal HttpContent Serialize() + { + JsonContent inviteContent = new(this, Serialization.Default.InviteProperties); + + if (TargetUsers is not { } targetUsers) + return inviteContent; + + return new MultipartFormDataContent() + { + { inviteContent, "payload_json" }, + { targetUsers.Serialize(), "target_users_file", "target_users_file" } + }; + } } diff --git a/NetCord/Rest/InviteTargetUsersJobStatus.cs b/NetCord/Rest/InviteTargetUsersJobStatus.cs new file mode 100644 index 00000000..a6486bf0 --- /dev/null +++ b/NetCord/Rest/InviteTargetUsersJobStatus.cs @@ -0,0 +1,28 @@ +using NetCord.Rest.JsonModels; + +namespace NetCord.Rest; + +public class InviteTargetUsersJobStatus(JsonInviteTargetUsersJobStatus jsonModel) : IJsonModel +{ + JsonInviteTargetUsersJobStatus IJsonModel.JsonModel => jsonModel; + + public InviteTargetUsersJobStatusCode Status => jsonModel.Status; + + public int TotalUsers => jsonModel.TotalUsers; + + public int ProcessedUsers => jsonModel.ProcessedUsers; + + public DateTimeOffset CreatedAt => jsonModel.CreatedAt; + + public DateTimeOffset? CompletedAt => jsonModel.CompletedAt; + + public string? ErrorMessage => jsonModel.ErrorMessage; +} + +public enum InviteTargetUsersJobStatusCode +{ + Unspecified = 0, + Processing = 1, + Completed = 2, + Failed = 3, +} diff --git a/NetCord/Rest/InviteTargetUsersProperties.cs b/NetCord/Rest/InviteTargetUsersProperties.cs new file mode 100644 index 00000000..ab632bcc --- /dev/null +++ b/NetCord/Rest/InviteTargetUsersProperties.cs @@ -0,0 +1,140 @@ +using System.Buffers.Text; +using System.Diagnostics.CodeAnalysis; + +namespace NetCord.Rest; + +[GenerateMethodsForProperties] +public partial class InviteTargetUsersProperties : IHttpSerializable +{ + private readonly Stream _stream; + private byte _read; + + private InviteTargetUsersProperties(Stream stream) + { + _stream = stream; + } + + public static InviteTargetUsersProperties FromStream(Stream stream) => new(stream); + + public static InviteTargetUsersProperties FromEnumerable(IEnumerable userIds) => new(new UserIdsStream(userIds)); + + HttpContent IHttpSerializable.Serialize() => Serialize(); + + internal HttpContent Serialize() + { + if (Interlocked.Exchange(ref _read, 1) is 1) + ThrowAlreadySent(); + + return new StreamContent(_stream); + } + + [DoesNotReturn] + private static void ThrowAlreadySent() + { + throw new InvalidOperationException("The invite target users have already been sent."); + } + + private sealed class UserIdsStream(IEnumerable userIds) : Stream + { + private readonly IEnumerator _enumerator = userIds.GetEnumerator(); + private readonly byte[] _buffer = new byte[22]; + private int _startPosition; + private int _endPosition; + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotSupportedException(); + + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override void Flush() => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) => Read(new Span(buffer, offset, count)); + + public override int Read(Span buffer) + { + int totalWritten = 0; + + if (_startPosition != _endPosition) + { + var bufferedLength = _endPosition - _startPosition; + + var length = Math.Min(bufferedLength, buffer.Length); + + _buffer.AsSpan(_startPosition, length).CopyTo(buffer); + _startPosition += length; + + if (length != bufferedLength) + return length; + + totalWritten += length; + } + + var newLine = "\r\n"u8; + + while (_enumerator.MoveNext()) + { + var userId = _enumerator.Current; + if (!Utf8Formatter.TryFormat(userId, buffer[totalWritten..], out var written)) + { + _ = Utf8Formatter.TryFormat(userId, _buffer, out var writtenToBuffer); + + _buffer.AsSpan(0, _startPosition = written = buffer.Length - totalWritten).CopyTo(buffer[totalWritten..]); + + totalWritten += written; + + newLine.CopyTo(_buffer.AsSpan(writtenToBuffer)); + _endPosition = writtenToBuffer + newLine.Length; + break; + } + + totalWritten += written; + + if (!newLine.TryCopyTo(buffer[totalWritten..])) + { + var remaining = buffer.Length - totalWritten; + if (remaining > 0) + { + newLine[..remaining].CopyTo(buffer[totalWritten..]); + totalWritten += remaining; + } + + newLine[remaining..].CopyTo(_buffer); + _startPosition = 0; + _endPosition = newLine.Length - remaining; + + break; + } + + totalWritten += newLine.Length; + } + + return totalWritten; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return new(Read(buffer.Span)); + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (disposing) + _enumerator.Dispose(); + } + } +} + diff --git a/NetCord/Rest/JsonModels/JsonInviteTargetUsersJobStatus.cs b/NetCord/Rest/JsonModels/JsonInviteTargetUsersJobStatus.cs new file mode 100644 index 00000000..0846b453 --- /dev/null +++ b/NetCord/Rest/JsonModels/JsonInviteTargetUsersJobStatus.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace NetCord.Rest.JsonModels; + +public class JsonInviteTargetUsersJobStatus +{ + [JsonPropertyName("status")] + public InviteTargetUsersJobStatusCode Status { get; set; } + + [JsonPropertyName("total_users")] + public int TotalUsers { get; set; } + + [JsonPropertyName("processed_users")] + public int ProcessedUsers { get; set; } + + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("completed_at")] + public DateTimeOffset? CompletedAt { get; set; } + + [JsonPropertyName("error_message")] + public string? ErrorMessage { get; set; } +} diff --git a/NetCord/Rest/JsonModels/JsonRestInvite.cs b/NetCord/Rest/JsonModels/JsonRestInvite.cs index 7133faaa..fd007431 100644 --- a/NetCord/Rest/JsonModels/JsonRestInvite.cs +++ b/NetCord/Rest/JsonModels/JsonRestInvite.cs @@ -39,12 +39,15 @@ public class JsonRestInvite [JsonPropertyName("expires_at")] public DateTimeOffset? ExpiresAt { get; set; } - [JsonPropertyName("stage_instance")] - public JsonStageInstance? StageInstance { get; set; } - [JsonPropertyName("guild_scheduled_event")] public JsonGuildScheduledEvent? GuildScheduledEvent { get; set; } + [JsonPropertyName("flags")] + public InviteFlags? Flags { get; set; } + + [JsonPropertyName("roles")] + public JsonRole[]? Roles { get; set; } + [JsonPropertyName("uses")] public int? Uses { get; set; } diff --git a/NetCord/Rest/RestClient.Channel.cs b/NetCord/Rest/RestClient.Channel.cs index 6b018d5f..ed9f1acc 100644 --- a/NetCord/Rest/RestClient.Channel.cs +++ b/NetCord/Rest/RestClient.Channel.cs @@ -436,7 +436,10 @@ public async Task> GetGuildChannelInvitesAsync(ulong cha [GenerateAlias([typeof(IGuildChannel)], nameof(IGuildChannel.Id))] public async Task CreateGuildChannelInviteAsync(ulong channelId, InviteProperties? inviteProperties = null, RestRequestProperties? properties = null, CancellationToken cancellationToken = default) { - using (HttpContent content = new JsonContent(inviteProperties, Serialization.Default.InviteProperties)) + if (inviteProperties is null) + return new(await (await SendRequestAsync(HttpMethod.Post, $"/channels/{channelId}/invites", null, new(channelId), properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonRestInvite).ConfigureAwait(false), this); + + using (HttpContent content = inviteProperties.Serialize()) return new(await (await SendRequestAsync(HttpMethod.Post, content, $"/channels/{channelId}/invites", null, new(channelId), properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonRestInvite).ConfigureAwait(false), this); } diff --git a/NetCord/Rest/RestClient.Invite.cs b/NetCord/Rest/RestClient.Invite.cs index 933c7cf3..c9c2c421 100644 --- a/NetCord/Rest/RestClient.Invite.cs +++ b/NetCord/Rest/RestClient.Invite.cs @@ -1,3 +1,6 @@ +using System.Buffers; +using System.Runtime.CompilerServices; + using NetCord.Gateway; namespace NetCord.Rest; @@ -5,7 +8,7 @@ namespace NetCord.Rest; public partial class RestClient { [GenerateAlias([typeof(RestInvite)], nameof(RestInvite.Code), TypeNameOverride = nameof(Invite))] - public async Task GetGuildInviteAsync(string inviteCode, bool withCounts = false, bool withExpiration = false, ulong? guildScheduledEventId = null, RestRequestProperties? properties = null, CancellationToken cancellationToken = default) + public async Task GetInviteAsync(string inviteCode, bool withCounts = false, bool withExpiration = false, ulong? guildScheduledEventId = null, RestRequestProperties? properties = null, CancellationToken cancellationToken = default) { if (guildScheduledEventId.HasValue) return new(await (await SendRequestAsync(HttpMethod.Get, $"/invites/{inviteCode}", $"?with_counts={withCounts}&with_expiration={withExpiration}&guild_scheduled_event_id={guildScheduledEventId}", null, properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonRestInvite).ConfigureAwait(false), this); @@ -14,6 +17,97 @@ public async Task GetGuildInviteAsync(string inviteCode, bool withCo } [GenerateAlias([typeof(RestInvite)], nameof(RestInvite.Code), TypeNameOverride = nameof(Invite))] - public async Task DeleteGuildInviteAsync(string inviteCode, RestRequestProperties? properties = null, CancellationToken cancellationToken = default) + public async Task DeleteInviteAsync(string inviteCode, RestRequestProperties? properties = null, CancellationToken cancellationToken = default) => new(await (await SendRequestAsync(HttpMethod.Delete, $"/invites/{inviteCode}", null, null, properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonRestInvite).ConfigureAwait(false), this); + + [GenerateAlias([typeof(RestInvite)], nameof(RestInvite.Code), TypeNameOverride = nameof(Invite))] + public async IAsyncEnumerable GetInviteTargetUsersAsync(string inviteCode, RestRequestProperties? properties = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var stream = await SendRequestAsync(HttpMethod.Get, $"/invites/{inviteCode}/target-users", null, null, properties, cancellationToken: cancellationToken).ConfigureAwait(false); + + var array = ArrayPool.Shared.Rent(4096); + var buffer = array.AsMemory(); + + try + { + int processingIndex; + int processingEndIndex; + + // Skip header + while (true) + { + int bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + + if (bytesRead is 0) + yield break; + + var span = buffer.Span[..bytesRead]; + + processingIndex = span.IndexOf((byte)'\n'); + + if (processingIndex >= 0) + { + processingEndIndex = bytesRead; + break; + } + } + + processingIndex++; + + while (true) + { + while (processingIndex < processingEndIndex) + { + var span = buffer.Span[processingIndex..processingEndIndex]; + + int end = span.IndexOf((byte)'\n'); + + if (end < 0) + break; + + var line = span[..end]; + + if (line.EndsWith((byte)'\r')) + line = line[..^1]; + + yield return Snowflake.Parse(line); + + processingIndex += end + 1; + } + + buffer.Span[processingIndex..processingEndIndex].CopyTo(buffer.Span); + + int bytesRead = await stream.ReadAsync(buffer[(processingEndIndex - processingIndex)..], cancellationToken).ConfigureAwait(false); + + if (bytesRead is 0) + { + var span = buffer.Span[..(processingEndIndex - processingIndex)]; + if (!span.IsEmpty) + yield return Snowflake.Parse(span); + + break; + } + + processingEndIndex = bytesRead + processingEndIndex - processingIndex; + processingIndex = 0; + } + } + finally + { + ArrayPool.Shared.Return(array); + await stream.DisposeAsync().ConfigureAwait(false); + } + } + + public async Task UpdateInviteTargetUsersAsync(string inviteCode, InviteTargetUsersProperties inviteTargetUsersProperties, RestRequestProperties? requestProperties = null, CancellationToken cancellationToken = default) + { + using (HttpContent content = new MultipartFormDataContent() + { + { inviteTargetUsersProperties.Serialize(), "target_users_file", "target_users_file" } + }) + await SendRequestAsync(HttpMethod.Put, content, $"/invites/{inviteCode}/target-users", null, null, requestProperties, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task GetInviteTargetUsersJobStatusAsync(string inviteCode, RestRequestProperties? properties = null, CancellationToken cancellationToken = default) + => new(await (await SendRequestAsync(HttpMethod.Get, $"/invites/{inviteCode}/target-users/job-status", null, null, properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonInviteTargetUsersJobStatus).ConfigureAwait(false)); } diff --git a/NetCord/Rest/RestGuildInvite.cs b/NetCord/Rest/RestInvite.cs similarity index 71% rename from NetCord/Rest/RestGuildInvite.cs rename to NetCord/Rest/RestInvite.cs index e88ccb22..18be1a36 100644 --- a/NetCord/Rest/RestGuildInvite.cs +++ b/NetCord/Rest/RestInvite.cs @@ -29,10 +29,13 @@ public partial class RestInvite : IInvite, IJsonModel public DateTimeOffset? ExpiresAt => _jsonModel.ExpiresAt; - public StageInstance? StageInstance { get; } - public GuildScheduledEvent? GuildScheduledEvent { get; } + public InviteFlags? Flags => _jsonModel.Flags; + + public IReadOnlyList? Roles { get; } + + // Metadata public int? Uses => _jsonModel.Uses; public int? MaxUses => _jsonModel.MaxUses; @@ -51,32 +54,29 @@ public RestInvite(JsonModels.JsonRestInvite jsonModel, RestClient client) { _jsonModel = jsonModel; - var guild = jsonModel.Guild; - if (guild is not null) + if (jsonModel.Guild is { } guild) + { Guild = new(guild, client); - var channel = jsonModel.Channel; - if (channel is not null) + var guildId = Guild.Id; + + if (jsonModel.Roles is { } roles) + Roles = roles.Select(role => new Role(role, guildId, client)).ToArray(); + } + + if (jsonModel.Channel is { } channel) Channel = Channel.CreateFromJson(channel, client); - var inviter = jsonModel.Inviter; - if (inviter is not null) + if (jsonModel.Inviter is { } inviter) Inviter = new(inviter, client); - var targetUser = jsonModel.TargetUser; - if (targetUser is not null) + if (jsonModel.TargetUser is { } targetUser) TargetUser = new(targetUser, client); - var targetApplication = jsonModel.TargetApplication; - if (targetApplication is not null) + if (jsonModel.TargetApplication is { } targetApplication) TargetApplication = new(targetApplication, client); - var stageInstance = jsonModel.StageInstance; - if (stageInstance is not null) - StageInstance = new(stageInstance, client); - - var guildScheduledEvent = jsonModel.GuildScheduledEvent; - if (guildScheduledEvent is not null) + if (jsonModel.GuildScheduledEvent is { } guildScheduledEvent) GuildScheduledEvent = new(guildScheduledEvent, client); _client = client; diff --git a/NetCord/Serialization.cs b/NetCord/Serialization.cs index 2b07ebb6..4bfdb709 100644 --- a/NetCord/Serialization.cs +++ b/NetCord/Serialization.cs @@ -284,4 +284,5 @@ namespace NetCord; [JsonSerializable(typeof(JsonRadioGroupComponent))] [JsonSerializable(typeof(JsonCheckboxGroupComponent))] [JsonSerializable(typeof(JsonCheckboxComponent))] +[JsonSerializable(typeof(JsonInviteTargetUsersJobStatus))] internal partial class Serialization : JsonSerializerContext; diff --git a/Tests/NetCord.Rest.Tests/AssemblyInfo.cs b/Tests/NetCord.Rest.Tests/AssemblyInfo.cs new file mode 100644 index 00000000..300f5b1a --- /dev/null +++ b/Tests/NetCord.Rest.Tests/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/Tests/NetCord.Rest.Tests/GetInviteTargetUsersTest.cs b/Tests/NetCord.Rest.Tests/GetInviteTargetUsersTest.cs new file mode 100644 index 00000000..8a12bfc5 --- /dev/null +++ b/Tests/NetCord.Rest.Tests/GetInviteTargetUsersTest.cs @@ -0,0 +1,95 @@ +using System.Net; +using System.Text; + +namespace NetCord.Rest.Tests; + +[TestClass] +public class GetInviteTargetUsersTest(TestContext context) +{ + private static object[] CreateData(IEnumerable userIds, string newLine, bool finalNewLine) + { + MemoryStream stream = new(); + using StreamWriter writer = new(stream, Encoding.UTF8, leaveOpen: true); + + writer.Write("user_id"); + + foreach (var userId in userIds) + { + writer.Write(newLine); + writer.Write(userId); + } + + if (finalNewLine) + writer.Write(newLine); + + writer.Flush(); + + stream.Position = 0; + return [userIds, stream]; + } + + public static IEnumerable GetInviteTargetUsersData() + { + return from separator in (IEnumerable)["\r\n", "\n"] + from finalNewLine in (IEnumerable)[true, false] + from data in + (IEnumerable)[ + CreateData(CreateUserIds(0), separator, finalNewLine), + CreateData(CreateUserIds(1), separator, finalNewLine), + CreateData(CreateUserIds(3), separator, finalNewLine), + CreateData(CreateUserIds(100), separator, finalNewLine), + CreateData(CreateUserIds(100000), separator, finalNewLine), + CreateData(CreateUserIds(1000000), separator, finalNewLine), + ] + select data; + + static IEnumerable CreateUserIds(int count) + { + var baseSnowflake = Snowflake.Create(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero), 31, 12, 2332); + + return Enumerable.Range(0, count).Select(i => (ulong)i + baseSnowflake); + } + } + + [TestMethod] + [DynamicData(nameof(GetInviteTargetUsersData))] + public async Task GetInviteTargetUsersAsync(IEnumerable userIds, Stream stream) + { + using RestClient client = new(new RestClientConfiguration + { + RequestHandler = new GetInviteTargetUsersRequestHandler(stream), + }); + + using var expectedEnumerator = userIds.GetEnumerator(); + + await foreach (var userId in client.GetInviteTargetUsersAsync("inviteCode", cancellationToken: context.CancellationToken).ConfigureAwait(false)) + { + Assert.IsTrue(expectedEnumerator.MoveNext()); + Assert.AreEqual(expectedEnumerator.Current, userId); + } + + Assert.IsFalse(expectedEnumerator.MoveNext()); + } + + private class GetInviteTargetUsersRequestHandler(Stream responseStream) : IRestRequestHandler + { + public void AddDefaultHeader(string name, IEnumerable values) + { + } + + public void Dispose() + { + responseStream.Dispose(); + } + + public Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + return Task.FromResult(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StreamContent(responseStream), + }); + } + } +} + diff --git a/Tests/NetCord.Rest.Tests/InviteTargetUsersPropertiesTest.cs b/Tests/NetCord.Rest.Tests/InviteTargetUsersPropertiesTest.cs new file mode 100644 index 00000000..93a7cc94 --- /dev/null +++ b/Tests/NetCord.Rest.Tests/InviteTargetUsersPropertiesTest.cs @@ -0,0 +1,35 @@ +namespace NetCord.Rest.Tests; + +[TestClass] +public class InviteTargetUsersPropertiesTest(TestContext context) +{ + public static IEnumerable FromEnumerableSerializationData() + { + var baseSnowflake = Snowflake.Create(new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero), 31, 12, 2332); + + object[] Convert(IEnumerable userIds) => [userIds.Select(u => u + baseSnowflake)]; + + yield return Convert([]); + yield return Convert([1]); + yield return Convert([1, 2, 3]); + yield return Convert(Enumerable.Range(0, 100).Select(i => (ulong)i)); + yield return Convert(Enumerable.Range(0, 100000).Select(i => (ulong)i)); + yield return Convert(Enumerable.Range(0, 1000000).Select(i => (ulong)i)); + } + + [TestMethod] + [DynamicData(nameof(FromEnumerableSerializationData))] + public async Task FromEnumerableSerializationAsync(IEnumerable userIds) + { + var properties = InviteTargetUsersProperties.FromEnumerable(userIds); + using var content = ((IHttpSerializable)properties).Serialize(); + + var output = await content.ReadAsStringAsync(context.CancellationToken).ConfigureAwait(false); + + var split = output.Split("\r\n"); + + Assert.AreEqual(split[^1], string.Empty); + + CollectionAssert.AreEqual(split.SkipLast(1).Select(l => Snowflake.Parse(l)).ToArray(), userIds.ToArray()); + } +} diff --git a/Tests/NetCord.Rest.Tests/NetCord.Rest.Tests.csproj b/Tests/NetCord.Rest.Tests/NetCord.Rest.Tests.csproj new file mode 100644 index 00000000..4021a471 --- /dev/null +++ b/Tests/NetCord.Rest.Tests/NetCord.Rest.Tests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + +