Skip to content
This repository was archived by the owner on Sep 3, 2024. It is now read-only.

Commit 3dcd1ea

Browse files
stream metadata operations
1 parent 190f29e commit 3dcd1ea

13 files changed

Lines changed: 492 additions & 29 deletions
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,211 @@
11
namespace SqlStreamStore.HAL.Tests
22
{
3+
using System;
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Net.Http.Headers;
10+
using System.Threading.Tasks;
11+
using Newtonsoft.Json.Linq;
12+
using Shouldly;
13+
using SqlStreamStore.Streams;
14+
using Xunit;
15+
316
public class StreamMetadataTests
417
{
18+
private const string StreamId = "a-stream";
19+
520
private readonly SqlStreamStoreHalMiddlewareFixture _fixture;
621

722
public StreamMetadataTests()
823
{
924
_fixture = new SqlStreamStoreHalMiddlewareFixture();
1025
}
26+
27+
[Fact]
28+
public async Task get_metadata_when_metadata_stream_does_not_exist()
29+
{
30+
using(var response =
31+
await _fixture.HttpClient.SendAsync(
32+
new HttpRequestMessage(HttpMethod.Get, $"/streams/{StreamId}/metadata")))
33+
{
34+
response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
35+
36+
var resource = await response.AsHal();
37+
38+
((string) resource.State.streamId).ShouldBe(StreamId);
39+
((int) resource.State.metadataStreamVersion).ShouldBe(ExpectedVersion.NoStream);
40+
((int?) resource.State.maxAge).ShouldBe(default(int?));
41+
((int?) resource.State.maxCount).ShouldBe(default(int?));
42+
((string) resource.State.metaJson).ShouldBeNull();
43+
44+
resource.ShouldLink(Constants.Relations.Self, "metadata");
45+
resource.ShouldLink(Constants.Relations.Feed, "../");
46+
}
47+
}
48+
49+
[Fact]
50+
public async Task get_metadata_when_metadata_stream_does_exist()
51+
{
52+
using(await _fixture.HttpClient.SendAsync(
53+
new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}/metadata")
54+
{
55+
Content = new StringContent(JObject.FromObject(new
56+
{
57+
maxAge = 30,
58+
maxCount = 20,
59+
metaJson = new
60+
{
61+
type = "a-type"
62+
}
63+
}).ToString())
64+
{
65+
Headers =
66+
{
67+
ContentType = new MediaTypeHeaderValue("application/json")
68+
}
69+
}
70+
}))
71+
using(var response =
72+
await _fixture.HttpClient.SendAsync(
73+
new HttpRequestMessage(HttpMethod.Get, $"/streams/{StreamId}/metadata")))
74+
{
75+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
76+
77+
var resource = await response.AsHal();
78+
79+
((string) resource.State.streamId).ShouldBe(StreamId);
80+
((int) resource.State.metadataStreamVersion).ShouldBe(0);
81+
((int?) resource.State.maxAge).ShouldBe(30);
82+
((int?) resource.State.maxCount).ShouldBe(20);
83+
JToken.DeepEquals(JObject.Parse((string) resource.State.metaJson),
84+
JObject.FromObject(new
85+
{
86+
type = "a-type"
87+
})).ShouldBeTrue();
88+
89+
resource.ShouldLink(Constants.Relations.Self, "metadata");
90+
resource.ShouldLink(Constants.Relations.Feed, "../");
91+
}
92+
}
93+
94+
[Fact]
95+
public async Task set_metadata()
96+
{
97+
using(var response = await _fixture.HttpClient.SendAsync(
98+
new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}/metadata")
99+
{
100+
Content = new StringContent(JObject.FromObject(new
101+
{
102+
maxAge = 30,
103+
maxCount = 20,
104+
metaJson = new
105+
{
106+
type = "a-type"
107+
}
108+
}).ToString())
109+
{
110+
Headers =
111+
{
112+
ContentType = new MediaTypeHeaderValue("application/json")
113+
}
114+
}
115+
}))
116+
{
117+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
118+
119+
var resource = await response.AsHal();
120+
121+
((string) resource.State.streamId).ShouldBe(StreamId);
122+
((int?) resource.State.maxAge).ShouldBe(30);
123+
((int?) resource.State.maxCount).ShouldBe(20);
124+
JToken.DeepEquals(JObject.Parse((string) resource.State.metaJson),
125+
JObject.FromObject(new
126+
{
127+
type = "a-type"
128+
})).ShouldBeTrue();
129+
130+
resource.ShouldLink(Constants.Relations.Self, "metadata");
131+
resource.ShouldLink(Constants.Relations.Feed, "../");
132+
}
133+
}
134+
135+
private static IEnumerable<int[]> WrongExpectedVersions()
136+
{
137+
yield return new[] { ExpectedVersion.NoStream, ExpectedVersion.NoStream };
138+
yield return new[] { ExpectedVersion.NoStream, 2 };
139+
}
140+
141+
public static IEnumerable<object[]> WrongExpectedVersionCases()
142+
=> WrongExpectedVersions().Select(s => new object[] { s });
143+
144+
[Theory]
145+
[MemberData(nameof(WrongExpectedVersionCases))]
146+
public async Task set_wrong_expected_version(int[] expectedVersions)
147+
{
148+
for(var i = 0; i < expectedVersions.Length - 1; i++)
149+
{
150+
using(await _fixture.HttpClient.SendAsync(
151+
new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}/metadata")
152+
{
153+
Headers =
154+
{
155+
{ Constants.Headers.ExpectedVersion, $"{expectedVersions[i]}" }
156+
},
157+
Content = new StringContent(JObject.FromObject(new
158+
{
159+
maxAge = 30,
160+
maxCount = 20,
161+
metaJson = new
162+
{
163+
type = "a-type"
164+
}
165+
}).ToString())
166+
{
167+
Headers =
168+
{
169+
ContentType = new MediaTypeHeaderValue("application/json")
170+
}
171+
}
172+
}))
173+
{ }
174+
}
175+
176+
using(var response = await _fixture.HttpClient.SendAsync(
177+
new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}/metadata")
178+
{
179+
Headers =
180+
{
181+
{ Constants.Headers.ExpectedVersion, $"{expectedVersions[expectedVersions.Length - 1]}" }
182+
},
183+
Content = new StringContent(JObject.FromObject(new
184+
{
185+
maxAge = 30,
186+
maxCount = 20,
187+
metaJson = new
188+
{
189+
type = "a-type"
190+
}
191+
}).ToString())
192+
{
193+
Headers =
194+
{
195+
ContentType = new MediaTypeHeaderValue("application/json")
196+
}
197+
}
198+
}))
199+
{
200+
response.StatusCode.ShouldBe(HttpStatusCode.Conflict);
201+
response.Content.Headers.ContentType.ShouldBe(new MediaTypeHeaderValue(
202+
Constants.Headers.ContentTypes.HalJson));
203+
}
204+
205+
var page = await _fixture.StreamStore.ReadStreamForwards($"$${StreamId}", 0, int.MaxValue);
206+
207+
page.Status.ShouldBe(PageReadStatus.Success);
208+
page.Messages.Length.ShouldBe(expectedVersions.Length - 1);
209+
}
11210
}
12211
}

src/SqlStreamStore.HAL/AppendStreamOptions.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,7 @@ private AppendStreamOptions(IOwinRequest request)
3737
{
3838
StreamId = request.Path.Value.Remove(0, 1);
3939

40-
ExpectedVersion = int.TryParse(
41-
request.Headers.Get(Constants.Headers.ExpectedVersion),
42-
out var expectedVersion)
43-
? expectedVersion
44-
: Streams.ExpectedVersion.Any;
40+
ExpectedVersion = request.GetExpectedVersion();
4541
}
4642

4743
private AppendStreamOptions(IOwinRequest request, JArray body)

src/SqlStreamStore.HAL/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static class Relations
3434
public static class Streams
3535
{
3636
public const string All = "stream";
37+
public const string Metadata = "metadata";
3738
}
3839

3940
public static IReadOnlyDictionary<int, string> ReasonPhrases { get; }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace SqlStreamStore.HAL
2+
{
3+
using System;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Owin;
7+
8+
internal class DeleteStreamMetadataOptions
9+
{
10+
public DeleteStreamMetadataOptions(IOwinRequest request)
11+
{
12+
StreamId = $"$${request.Path.Value.Remove(0, 1)}";
13+
14+
ExpectedVersion = request.GetExpectedVersion();
15+
}
16+
17+
public string StreamId { get; }
18+
public int ExpectedVersion { get; }
19+
20+
public Func<IStreamStore, CancellationToken, Task> GetDeleteOperation()
21+
=> (streamStore, ct) => streamStore.DeleteStream(StreamId, ExpectedVersion, ct);
22+
}
23+
}

src/SqlStreamStore.HAL/DeleteStreamOptions.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@ public DeleteStreamOptions(IOwinRequest request)
1111
{
1212
StreamId = request.Path.Value.Remove(0, 1);
1313

14-
ExpectedVersion = int.TryParse(
15-
request.Headers.Get(Constants.Headers.ExpectedVersion),
16-
out var expectedVersion)
17-
? expectedVersion
18-
: Streams.ExpectedVersion.Any;
14+
ExpectedVersion = request.GetExpectedVersion();
1915
}
2016

2117
public string StreamId { get; }
@@ -24,4 +20,4 @@ public DeleteStreamOptions(IOwinRequest request)
2420
public Func<IStreamStore, CancellationToken, Task> GetDeleteOperation()
2521
=> (streamStore, ct) => streamStore.DeleteStream(StreamId, ExpectedVersion, ct);
2622
}
27-
}
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace SqlStreamStore.HAL
2+
{
3+
using System;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Owin;
7+
using SqlStreamStore.Streams;
8+
9+
internal class GetStreamMetadataOptions
10+
{
11+
public GetStreamMetadataOptions(IOwinRequest request)
12+
{
13+
StreamId = request.Path.Value.Split('/')[1];
14+
}
15+
16+
public string StreamId { get; }
17+
18+
public Func<IReadonlyStreamStore, CancellationToken, Task<StreamMetadataResult>> GetReadOperation()
19+
=> (streamStore, ct) => streamStore.GetStreamMetadata(StreamId, ct);
20+
}
21+
}

src/SqlStreamStore.HAL/OwinContextExtensions.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,16 @@ public static void SetStandardCorsHeaders(this IOwinContext context, params Http
4242
{
4343
if(allowedMethods?.Length > 0)
4444
{
45-
context.Response.Headers.AppendValues("Access-Control-Allow-Methods",
45+
context.Response.Headers.AppendValues("Access-Control-Allow-Methods",
4646
allowedMethods.Select(_ => _.Method).ToArray());
4747
}
48-
48+
4949
context.Response.Headers.AppendValues(
5050
"Access-Control-Allow-Headers",
5151
"Content-Type",
5252
"X-Requested-With",
5353
"Authorization");
54-
54+
5555
context.Response.Headers.AppendValues("Access-Control-Allow-Origin", "*");
5656
}
5757

@@ -60,12 +60,18 @@ public static bool IsGetOrHead(this IOwinContext context)
6060

6161
public static bool IsPost(this IOwinContext context)
6262
=> context.Request.Method == "POST";
63-
63+
6464
public static bool IsDelete(this IOwinContext context)
6565
=> context.Request.Method == "DELETE";
6666

6767
public static bool IsOptions(this IOwinContext context)
6868
=> context.Request.Method == "OPTIONS";
6969

70+
public static int GetExpectedVersion(this IOwinRequest request)
71+
=> int.TryParse(
72+
request.Headers.Get(Constants.Headers.ExpectedVersion),
73+
out var expectedVersion)
74+
? expectedVersion
75+
: Streams.ExpectedVersion.Any;
7076
}
7177
}

src/SqlStreamStore.HAL/PathStringExtensions.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,23 @@ public static bool IsAllStream(this PathString requestPath)
88
=> !requestPath.HasValue;
99

1010
public static bool IsAllStreamMessage(this PathString requestPath)
11-
=> long.TryParse(requestPath.Value?.Remove(0, 1), out var _);
11+
=> long.TryParse(requestPath.Value?.Remove(0, 1), out _);
1212

1313
public static bool IsStream(this PathString requestPath)
14-
=> requestPath.Value?.Length > 1;
14+
=> requestPath.Value?.Split('/').Length == 2;
1515

16-
public static bool IsStreamMessage(this PathString requestPath)
17-
=> requestPath.Value?.Split('/')?.Length == 3;
16+
public static bool IsStreamMessage(this PathString requestPath)
17+
{
18+
var segments = requestPath.Value?.Split('/');
19+
20+
return segments?.Length == 3 && int.TryParse(segments[2], out _);
21+
}
22+
23+
public static bool IsStreamMetadata(this PathString requestPath)
24+
{
25+
var segments = requestPath.Value?.Split('/');
26+
27+
return segments?.Length == 3 && segments[2] == Constants.Streams.Metadata;
28+
}
1829
}
1930
}

0 commit comments

Comments
 (0)