Skip to content

Commit f693efe

Browse files
authored
Validate stream name parameters in JSContext methods. (#379)
* Add MacOS .DS_Store * Add stream name parameter validation in NatsJSContext.Streams * Fix test name * Add stream name parameter validation for consumer methods * Move validation methods * Fix warning * Add wilcard tests * Remove checking for > and * * Remove tests of * and > that were left behind * Align .net6 and .net8 exception messages --------- Co-authored-by: Niklas Petersen <niklasfp@users.noreply.github.com>
1 parent b68542e commit f693efe

File tree

8 files changed

+153
-2
lines changed

8 files changed

+153
-2
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,5 @@ nuget/*.unitypackage
117117
# NuGet package
118118
/dist
119119

120+
# MacOS folder attributes
121+
.DS_Store

src/NATS.Client.JetStream/INatsJSContext.cs

+12
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public interface INatsJSContext
1313
/// <param name="opts">Ordered consumer options.</param>
1414
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the API call.</param>
1515
/// <returns>The NATS JetStream consumer object which can be used retrieving ordered data from the stream.</returns>
16+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
17+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
1618
ValueTask<INatsJSConsumer> CreateOrderedConsumerAsync(
1719
string stream,
1820
NatsJSOrderedConsumerOpts? opts = default,
@@ -27,6 +29,8 @@ ValueTask<INatsJSConsumer> CreateOrderedConsumerAsync(
2729
/// <returns>The NATS JetStream consumer object which can be used retrieving data from the stream.</returns>
2830
/// <exception cref="NatsJSException">Ack policy is set to <c>none</c> or there was an issue retrieving the response.</exception>
2931
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
32+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
33+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
3034
ValueTask<INatsJSConsumer> CreateOrUpdateConsumerAsync(
3135
string stream,
3236
ConsumerConfig config,
@@ -41,6 +45,8 @@ ValueTask<INatsJSConsumer> CreateOrUpdateConsumerAsync(
4145
/// <returns>The NATS JetStream consumer object which can be used retrieving data from the stream.</returns>
4246
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
4347
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
48+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
49+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
4450
ValueTask<INatsJSConsumer> GetConsumerAsync(string stream, string consumer, CancellationToken cancellationToken = default);
4551

4652
/// <summary>
@@ -51,6 +57,8 @@ ValueTask<INatsJSConsumer> CreateOrUpdateConsumerAsync(
5157
/// <returns>Async enumerable of consumer info objects. Can be used in a <c>await foreach</c> loop.</returns>
5258
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
5359
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
60+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
61+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
5462
IAsyncEnumerable<INatsJSConsumer> ListConsumersAsync(
5563
string stream,
5664
CancellationToken cancellationToken = default);
@@ -63,6 +71,8 @@ IAsyncEnumerable<INatsJSConsumer> ListConsumersAsync(
6371
/// <returns>Async enumerable of consumer info objects. Can be used in a <c>await foreach</c> loop.</returns>
6472
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
6573
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
74+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
75+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
6676
IAsyncEnumerable<string> ListConsumerNamesAsync(
6777
string stream,
6878
CancellationToken cancellationToken = default);
@@ -76,6 +86,8 @@ IAsyncEnumerable<string> ListConsumerNamesAsync(
7686
/// <returns>Whether the deletion was successful.</returns>
7787
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
7888
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
89+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
90+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
7991
ValueTask<bool> DeleteConsumerAsync(string stream, string consumer, CancellationToken cancellationToken = default);
8092

8193
/// <summary>

src/NATS.Client.JetStream/NatsJSContext.Consumers.cs

+13
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ public partial class NatsJSContext : INatsJSContext
1414
/// <param name="opts">Ordered consumer options.</param>
1515
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the API call.</param>
1616
/// <returns>The NATS JetStream consumer object which can be used retrieving ordered data from the stream.</returns>
17+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
18+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
1719
public ValueTask<INatsJSConsumer> CreateOrderedConsumerAsync(
1820
string stream,
1921
NatsJSOrderedConsumerOpts? opts = default,
2022
CancellationToken cancellationToken = default)
2123
{
24+
ThrowIfInvalidStreamName(stream);
2225
opts ??= NatsJSOrderedConsumerOpts.Default;
2326
return new ValueTask<INatsJSConsumer>(new NatsJSOrderedConsumer(stream, this, opts, cancellationToken));
2427
}
@@ -29,6 +32,8 @@ public async ValueTask<INatsJSConsumer> CreateOrUpdateConsumerAsync(
2932
ConsumerConfig config,
3033
CancellationToken cancellationToken = default)
3134
{
35+
ThrowIfInvalidStreamName(stream);
36+
3237
// TODO: Adjust API subject according to server version and filter subject
3338
var subject = $"{Opts.Prefix}.CONSUMER.CREATE.{stream}";
3439

@@ -64,8 +69,11 @@ public async ValueTask<INatsJSConsumer> CreateOrUpdateConsumerAsync(
6469
/// <returns>The NATS JetStream consumer object which can be used retrieving data from the stream.</returns>
6570
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
6671
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
72+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
73+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
6774
public async ValueTask<INatsJSConsumer> GetConsumerAsync(string stream, string consumer, CancellationToken cancellationToken = default)
6875
{
76+
ThrowIfInvalidStreamName(stream);
6977
var response = await JSRequestResponseAsync<object, ConsumerInfo>(
7078
subject: $"{Opts.Prefix}.CONSUMER.INFO.{stream}.{consumer}",
7179
request: null,
@@ -78,6 +86,7 @@ public async IAsyncEnumerable<INatsJSConsumer> ListConsumersAsync(
7886
string stream,
7987
[EnumeratorCancellation] CancellationToken cancellationToken = default)
8088
{
89+
ThrowIfInvalidStreamName(stream);
8190
var offset = 0;
8291
while (!cancellationToken.IsCancellationRequested)
8392
{
@@ -103,6 +112,7 @@ public async IAsyncEnumerable<string> ListConsumerNamesAsync(
103112
string stream,
104113
[EnumeratorCancellation] CancellationToken cancellationToken = default)
105114
{
115+
ThrowIfInvalidStreamName(stream);
106116
var offset = 0;
107117
while (!cancellationToken.IsCancellationRequested)
108118
{
@@ -130,8 +140,11 @@ public async IAsyncEnumerable<string> ListConsumerNamesAsync(
130140
/// <returns>Whether the deletion was successful.</returns>
131141
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
132142
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
143+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
144+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
133145
public async ValueTask<bool> DeleteConsumerAsync(string stream, string consumer, CancellationToken cancellationToken = default)
134146
{
147+
ThrowIfInvalidStreamName(stream);
135148
var response = await JSRequestResponseAsync<object, ConsumerDeleteResponse>(
136149
subject: $"{Opts.Prefix}.CONSUMER.DELETE.{stream}.{consumer}",
137150
request: null,

src/NATS.Client.JetStream/NatsJSContext.Streams.cs

+18-2
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ public partial class NatsJSContext
1313
/// <returns>The NATS JetStream stream object which can be used to manage the stream.</returns>
1414
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
1515
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
16+
/// <exception cref="ArgumentException">The stream name in <paramref name="config"/> is invalid.</exception>
17+
/// <exception cref="ArgumentNullException">The name in <paramref name="config"/> is <c>null</c>.</exception>
1618
public async ValueTask<INatsJSStream> CreateStreamAsync(
1719
StreamConfig config,
1820
CancellationToken cancellationToken = default)
1921
{
20-
ArgumentNullException.ThrowIfNull(config.Name, nameof(config.Name));
22+
ThrowIfInvalidStreamName(config.Name, nameof(config.Name));
2123
var response = await JSRequestResponseAsync<StreamConfig, StreamInfo>(
2224
subject: $"{Opts.Prefix}.STREAM.CREATE.{config.Name}",
2325
config,
@@ -33,10 +35,13 @@ public async ValueTask<INatsJSStream> CreateStreamAsync(
3335
/// <returns>Whether delete was successful or not.</returns>
3436
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
3537
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
38+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
39+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
3640
public async ValueTask<bool> DeleteStreamAsync(
3741
string stream,
3842
CancellationToken cancellationToken = default)
3943
{
44+
ThrowIfInvalidStreamName(stream);
4045
var response = await JSRequestResponseAsync<object, StreamMsgDeleteResponse>(
4146
subject: $"{Opts.Prefix}.STREAM.DELETE.{stream}",
4247
request: null,
@@ -53,11 +58,14 @@ public async ValueTask<bool> DeleteStreamAsync(
5358
/// <returns>Purge response</returns>
5459
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
5560
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
61+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
62+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
5663
public async ValueTask<StreamPurgeResponse> PurgeStreamAsync(
5764
string stream,
5865
StreamPurgeRequest request,
5966
CancellationToken cancellationToken = default)
6067
{
68+
ThrowIfInvalidStreamName(stream);
6169
var response = await JSRequestResponseAsync<StreamPurgeRequest, StreamPurgeResponse>(
6270
subject: $"{Opts.Prefix}.STREAM.PURGE.{stream}",
6371
request: request,
@@ -74,11 +82,14 @@ public async ValueTask<StreamPurgeResponse> PurgeStreamAsync(
7482
/// <returns>Delete message response</returns>
7583
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
7684
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
85+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
86+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
7787
public async ValueTask<StreamMsgDeleteResponse> DeleteMessageAsync(
7888
string stream,
7989
StreamMsgDeleteRequest request,
8090
CancellationToken cancellationToken = default)
8191
{
92+
ThrowIfInvalidStreamName(stream);
8293
var response = await JSRequestResponseAsync<StreamMsgDeleteRequest, StreamMsgDeleteResponse>(
8394
subject: $"{Opts.Prefix}.STREAM.MSG.DELETE.{stream}",
8495
request: request,
@@ -95,11 +106,14 @@ public async ValueTask<StreamMsgDeleteResponse> DeleteMessageAsync(
95106
/// <returns>The NATS JetStream stream object which can be used to manage the stream.</returns>
96107
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
97108
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
109+
/// <exception cref="ArgumentException">The <paramref name="stream"/> name is invalid.</exception>
110+
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> name is <c>null</c>.</exception>
98111
public async ValueTask<INatsJSStream> GetStreamAsync(
99112
string stream,
100113
StreamInfoRequest? request = null,
101114
CancellationToken cancellationToken = default)
102115
{
116+
ThrowIfInvalidStreamName(stream);
103117
var response = await JSRequestResponseAsync<StreamInfoRequest, StreamInfoResponse>(
104118
subject: $"{Opts.Prefix}.STREAM.INFO.{stream}",
105119
request: request,
@@ -115,11 +129,13 @@ public async ValueTask<INatsJSStream> GetStreamAsync(
115129
/// <returns>The updated NATS JetStream stream object.</returns>
116130
/// <exception cref="NatsJSException">There was an issue retrieving the response.</exception>
117131
/// <exception cref="NatsJSApiException">Server responded with an error.</exception>
132+
/// <exception cref="ArgumentException">The stream name in <paramref name="request"/> is invalid.</exception>
133+
/// <exception cref="ArgumentNullException">The name in <paramref name="request"/> is <c>null</c>.</exception>
118134
public async ValueTask<NatsJSStream> UpdateStreamAsync(
119135
StreamConfig request,
120136
CancellationToken cancellationToken = default)
121137
{
122-
ArgumentNullException.ThrowIfNull(request.Name, nameof(request.Name));
138+
ThrowIfInvalidStreamName(request.Name, nameof(request.Name));
123139
var response = await JSRequestResponseAsync<StreamConfig, StreamUpdateResponse>(
124140
subject: $"{Opts.Prefix}.STREAM.UPDATE.{request.Name}",
125141
request: request,

src/NATS.Client.JetStream/NatsJSContext.cs

+25
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.Runtime.CompilerServices;
23
using Microsoft.Extensions.Logging;
34
using NATS.Client.Core;
@@ -172,6 +173,22 @@ public async ValueTask<PubAckResponse> PublishAsync<T>(
172173
throw new NatsJSPublishNoResponseException();
173174
}
174175

176+
internal static void ThrowIfInvalidStreamName([NotNull] string? name, [CallerArgumentExpression("name")] string? paramName = null)
177+
{
178+
ArgumentNullException.ThrowIfNull(name, paramName);
179+
180+
if (name.Length == 0)
181+
{
182+
ThrowEmptyException(paramName);
183+
}
184+
185+
var nameSpan = name.AsSpan();
186+
if (nameSpan.IndexOfAny(" .") >= 0)
187+
{
188+
ThrowInvalidStreamNameException(paramName);
189+
}
190+
}
191+
175192
internal string NewInbox() => NatsConnection.NewInbox(Connection.Opts.InboxPrefix);
176193

177194
internal async ValueTask<TResponse> JSRequestResponseAsync<TRequest, TResponse>(
@@ -237,4 +254,12 @@ internal async ValueTask<NatsJSResponse<TResponse>> JSRequestAsync<TRequest, TRe
237254

238255
throw new NatsJSApiNoResponseException();
239256
}
257+
258+
[DoesNotReturn]
259+
private static void ThrowInvalidStreamNameException(string? paramName) =>
260+
throw new ArgumentException("Stream name cannot contain ' ', '.'", paramName);
261+
262+
[DoesNotReturn]
263+
private static void ThrowEmptyException(string? paramName) =>
264+
throw new ArgumentException("The value cannot be an empty string.", paramName);
240265
}

tests/NATS.Client.JetStream.Tests/ConsumerConsumeTest.cs

+39
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,45 @@ public class ConsumerConsumeTest
1111

1212
public ConsumerConsumeTest(ITestOutputHelper output) => _output = output;
1313

14+
[Theory]
15+
[InlineData("Invalid.DotName")]
16+
[InlineData("Invalid SpaceName")]
17+
[InlineData(null)]
18+
public async Task Consumer_stream_invalid_name_test(string? streamName)
19+
{
20+
var jsmContext = new NatsJSContext(new NatsConnection());
21+
22+
var consumerConfig = new ConsumerConfig("aconsumer");
23+
24+
// Create ordered consumer
25+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.CreateOrderedConsumerAsync(streamName!, cancellationToken: CancellationToken.None));
26+
27+
// Create or update consumer
28+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.CreateOrUpdateConsumerAsync(streamName!, consumerConfig));
29+
30+
// Get consumer
31+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.GetConsumerAsync(streamName!, "aconsumer"));
32+
33+
// List consumers
34+
await Assert.ThrowsAnyAsync<ArgumentException>(async () =>
35+
{
36+
await foreach (var unused in jsmContext.ListConsumersAsync(streamName!, CancellationToken.None))
37+
{
38+
}
39+
});
40+
41+
// List consumer names
42+
await Assert.ThrowsAnyAsync<ArgumentException>(async () =>
43+
{
44+
await foreach (var unused in jsmContext.ListConsumerNamesAsync(streamName!, CancellationToken.None))
45+
{
46+
}
47+
});
48+
49+
// Delete consumer
50+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.DeleteConsumerAsync(streamName!, "aconsumer"));
51+
}
52+
1453
[Fact]
1554
public async Task Consume_msgs_test()
1655
{

tests/NATS.Client.JetStream.Tests/JetStreamTest.cs

+36
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,42 @@ public class JetStreamTest
99

1010
public JetStreamTest(ITestOutputHelper output) => _output = output;
1111

12+
[Theory]
13+
[InlineData("Invalid.DotName")]
14+
[InlineData("Invalid SpaceName")]
15+
[InlineData(null)]
16+
public async Task Stream_invalid_name_test(string? streamName)
17+
{
18+
var jsmContext = new NatsJSContext(new NatsConnection());
19+
20+
var cfg = new StreamConfig()
21+
{
22+
Name = streamName,
23+
Subjects = new[] { "events.*" },
24+
};
25+
26+
// Create stream
27+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.CreateStreamAsync(cfg));
28+
29+
// Delete stream
30+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.DeleteStreamAsync(streamName!));
31+
32+
// Get stream
33+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.GetStreamAsync(streamName!, null));
34+
35+
// Update stream
36+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.UpdateStreamAsync(cfg));
37+
38+
// Purge stream
39+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.PurgeStreamAsync(streamName!, new StreamPurgeRequest()));
40+
41+
// Get stream
42+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.GetStreamAsync(streamName!));
43+
44+
// Delete Messages
45+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await jsmContext.DeleteMessageAsync(streamName!, new StreamMsgDeleteRequest()));
46+
}
47+
1248
[Fact]
1349
public async Task Create_stream_test()
1450
{

tests/NATS.Client.JetStream.Tests/NatsJSContextTest.cs

+8
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,12 @@ public void InterfaceShouldHaveSamePublicPropertiesEventsAndMethodAsClass()
2727
interfaceMethods.Select(m => m.Name).Should().Contain(name);
2828
}
2929
}
30+
31+
[Fact]
32+
public void Invalid_stream_validation_test()
33+
{
34+
Assert.Throws<ArgumentNullException>(() => NatsJSContext.ThrowIfInvalidStreamName(null!));
35+
Assert.Throws<ArgumentException>(() => NatsJSContext.ThrowIfInvalidStreamName("Invalid.DotName"));
36+
Assert.Throws<ArgumentException>(() => NatsJSContext.ThrowIfInvalidStreamName("Invalid SpaceName"));
37+
}
3038
}

0 commit comments

Comments
 (0)