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

Commit c8a4199

Browse files
Merge pull request #33 from thefringeninja/201
Fix Inconsistencies When Creating Stream
2 parents 43e992c + e72e7ae commit c8a4199

3 files changed

Lines changed: 156 additions & 73 deletions

File tree

src/SqlStreamStore.HAL.Tests/StreamAppendTests.cs

Lines changed: 134 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public StreamAppendTests()
2424

2525
public static IEnumerable<object[]> AppendCases()
2626
{
27-
var messageId = Guid.NewGuid();
27+
var messageId1 = Guid.NewGuid();
28+
var messageId2 = Guid.NewGuid();
2829

2930
var jsonData = JObject.FromObject(new
3031
{
@@ -36,69 +37,83 @@ public static IEnumerable<object[]> AppendCases()
3637
property = "metaValue"
3738
});
3839

39-
var bodies = new JToken[]
40+
var bodies = new (JToken, Guid[])[]
4041
{
41-
JObject.FromObject(new
42+
(JObject.FromObject(new
4243
{
43-
messageId,
44+
messageId = messageId1,
4445
type = "type",
4546
jsonData,
4647
jsonMetadata
47-
}),
48-
JArray.FromObject(new[]
49-
{
50-
new
48+
}), new[] { messageId1 }),
49+
(JArray.FromObject(new[]
5150
{
52-
messageId,
53-
type = "type",
54-
jsonData,
55-
jsonMetadata
56-
}
57-
})
51+
new
52+
{
53+
messageId = messageId1,
54+
type = "type",
55+
jsonData,
56+
jsonMetadata
57+
},
58+
new
59+
{
60+
messageId = messageId2,
61+
type = "type",
62+
jsonData,
63+
jsonMetadata
64+
}
65+
}),
66+
new[] { messageId1, messageId2 })
5867
};
5968

60-
foreach(var body in bodies)
69+
var location = new Uri($"../{Constants.Streams.Stream}/{StreamId}", UriKind.Relative);
70+
71+
foreach(var (body, messageIds) in bodies)
6172
{
6273
yield return new object[]
6374
{
64-
body,
6575
ExpectedVersion.Any,
66-
HttpStatusCode.Created,
67-
messageId,
76+
body,
77+
messageIds,
6878
jsonData,
69-
jsonMetadata
79+
jsonMetadata,
80+
HttpStatusCode.OK,
81+
null
7082
};
7183
yield return new object[]
7284
{
73-
body,
7485
default(int?),
75-
HttpStatusCode.Created,
76-
messageId,
86+
body,
87+
messageIds,
7788
jsonData,
78-
jsonMetadata
89+
jsonMetadata,
90+
HttpStatusCode.OK,
91+
null
7992
};
8093
yield return new object[]
8194
{
82-
body,
8395
ExpectedVersion.NoStream,
84-
HttpStatusCode.Created,
85-
messageId,
96+
body,
97+
messageIds,
8698
jsonData,
87-
jsonMetadata
99+
jsonMetadata,
100+
HttpStatusCode.Created,
101+
location
88102
};
89103
}
90104
}
91105

92106
[Theory, MemberData(nameof(AppendCases))]
93107
public async Task expected_version(
94-
JToken body,
95108
int? expectedVersion,
96-
HttpStatusCode statusCode,
97-
Guid messageId,
109+
JToken body,
110+
Guid[] messageIds,
98111
JObject jsonData,
99-
JObject jsonMetadata)
112+
JObject jsonMetadata,
113+
HttpStatusCode statusCode,
114+
Uri location)
100115
{
101-
var request = new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}")
116+
var request = new HttpRequestMessage(HttpMethod.Post, $"/{Constants.Streams.Stream}/{StreamId}")
102117
{
103118
Content = new StringContent(body.ToString())
104119
};
@@ -111,21 +126,25 @@ public async Task expected_version(
111126
using(var response = await _fixture.HttpClient.SendAsync(request))
112127
{
113128
response.StatusCode.ShouldBe(statusCode);
129+
response.Headers.Location.ShouldBe(location);
114130
}
115131

116132
var page = await _fixture.StreamStore.ReadStreamForwards(StreamId, 0, int.MaxValue);
117-
133+
118134
page.Status.ShouldBe(PageReadStatus.Success);
119-
page.Messages.Length.ShouldBe(1);
120-
page.Messages[0].MessageId.ShouldBe(messageId);
121-
page.Messages[0].Type.ShouldBe("type");
122-
JToken.DeepEquals(JObject.Parse(await page.Messages[0].GetJsonData()), jsonData).ShouldBeTrue();
123-
JToken.DeepEquals(JObject.Parse(page.Messages[0].JsonMetadata), jsonMetadata).ShouldBeTrue();
135+
page.Messages.Length.ShouldBe(messageIds.Length);
136+
for(var i = 0; i < messageIds.Length; i++)
137+
{
138+
page.Messages[i].MessageId.ShouldBe(messageIds[i]);
139+
page.Messages[i].Type.ShouldBe("type");
140+
JToken.DeepEquals(JObject.Parse(await page.Messages[i].GetJsonData()), jsonData).ShouldBeTrue();
141+
JToken.DeepEquals(JObject.Parse(page.Messages[i].JsonMetadata), jsonMetadata).ShouldBeTrue();
142+
}
124143
}
125144

126145
[Theory]
127-
[InlineData(new[]{ExpectedVersion.NoStream, ExpectedVersion.NoStream})]
128-
[InlineData(new[]{ExpectedVersion.NoStream, 2})]
146+
[InlineData(new[] { ExpectedVersion.NoStream, ExpectedVersion.NoStream })]
147+
[InlineData(new[] { ExpectedVersion.NoStream, 2 })]
129148
public async Task wrong_expected_version(int[] expectedVersions)
130149
{
131150
var jsonData = JObject.FromObject(new
@@ -140,11 +159,36 @@ public async Task wrong_expected_version(int[] expectedVersions)
140159

141160
for(var i = 0; i < expectedVersions.Length - 1; i++)
142161
{
143-
using(await _fixture.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}")
162+
using(await _fixture.HttpClient.SendAsync(
163+
new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}")
164+
{
165+
Headers =
166+
{
167+
{ Constants.Headers.ExpectedVersion, $"{expectedVersions[i]}" }
168+
},
169+
Content = new StringContent(JObject.FromObject(new
170+
{
171+
messageId = Guid.NewGuid(),
172+
type = "type",
173+
jsonData,
174+
jsonMetadata
175+
}).ToString())
176+
{
177+
Headers =
178+
{
179+
ContentType = new MediaTypeHeaderValue("application/json")
180+
}
181+
}
182+
}))
183+
{ }
184+
}
185+
186+
using(var response = await _fixture.HttpClient.SendAsync(
187+
new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}")
144188
{
145189
Headers =
146190
{
147-
{Constants.Headers.ExpectedVersion, $"{expectedVersions[i]}"}
191+
{ Constants.Headers.ExpectedVersion, $"{expectedVersions[expectedVersions.Length - 1]}" }
148192
},
149193
Content = new StringContent(JObject.FromObject(new
150194
{
@@ -160,46 +204,24 @@ public async Task wrong_expected_version(int[] expectedVersions)
160204
}
161205
}
162206
}))
163-
{ }
164-
}
165-
166-
using(var response = await _fixture.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}")
167-
{
168-
Headers =
169-
{
170-
{Constants.Headers.ExpectedVersion, $"{expectedVersions[expectedVersions.Length - 1]}"}
171-
},
172-
Content = new StringContent(JObject.FromObject(new
173-
{
174-
messageId = Guid.NewGuid(),
175-
type = "type",
176-
jsonData,
177-
jsonMetadata
178-
}).ToString())
179-
{
180-
Headers =
181-
{
182-
ContentType = new MediaTypeHeaderValue("application/json")
183-
}
184-
}
185-
}))
186207
{
187208
response.StatusCode.ShouldBe(HttpStatusCode.Conflict);
188209
response.Content.Headers.ContentType.ShouldBe(new MediaTypeHeaderValue(
189210
Constants.MediaTypes.HalJson));
190211
}
212+
191213
var page = await _fixture.StreamStore.ReadStreamForwards(StreamId, 0, int.MaxValue);
192-
214+
193215
page.Status.ShouldBe(PageReadStatus.Success);
194216
page.Messages.Length.ShouldBe(expectedVersions.Length - 1);
195217
}
196218

197219
private static IEnumerable<string> MalformedRequests()
198220
{
199221
var messageId = Guid.NewGuid();
200-
222+
201223
const string type = "type";
202-
224+
203225
var jsonData = JObject.FromObject(new
204226
{
205227
property = "value"
@@ -209,18 +231,18 @@ private static IEnumerable<string> MalformedRequests()
209231
{
210232
property = "metaValue"
211233
});
212-
234+
213235
yield return string.Empty;
214236
yield return "{}";
215-
237+
216238
yield return JObject.FromObject(new
217239
{
218240
messageId = Guid.Empty,
219241
type,
220242
jsonData,
221243
jsonMetadata
222244
}).ToString();
223-
245+
224246
yield return JObject.FromObject(new
225247
{
226248
type,
@@ -258,6 +280,47 @@ public async Task malformed_request_body(string malformedRequest)
258280
}
259281
}
260282

283+
public static IEnumerable<object[]> BadExpectedVersionCases()
284+
{
285+
var random = new Random();
286+
287+
var maximumBadExpectedVersion = Constants.Headers.MinimumExpectedVersion - 1;
288+
289+
yield return new object[] { maximumBadExpectedVersion };
290+
for(var i = 0; i < 5; i++)
291+
{
292+
yield return new object[] { random.Next(int.MinValue, maximumBadExpectedVersion) };
293+
}
294+
}
295+
296+
[Theory, MemberData(nameof(BadExpectedVersionCases))]
297+
public async Task bad_expected_version(int badExpectedVersion)
298+
{
299+
using(var response = await _fixture.HttpClient.SendAsync(
300+
new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}")
301+
{
302+
Headers =
303+
{
304+
{Constants.Headers.ExpectedVersion, $"{badExpectedVersion}"}
305+
},
306+
Content = new StringContent(JObject.FromObject(new
307+
{
308+
messageId = Guid.NewGuid(),
309+
jsonData = new { },
310+
type = "type"
311+
}).ToString())
312+
{
313+
Headers =
314+
{
315+
ContentType = new MediaTypeHeaderValue(Constants.MediaTypes.HalJson)
316+
}
317+
}
318+
}))
319+
{
320+
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
321+
}
322+
}
323+
261324
public void Dispose() => _fixture.Dispose();
262325
}
263326
}

src/SqlStreamStore.HAL/Constants.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
namespace SqlStreamStore.HAL
22
{
3+
using System.Linq;
4+
using System.Reflection;
35
using Microsoft.AspNetCore.Http;
6+
using SqlStreamStore.Streams;
47

58
internal static class Constants
69
{
@@ -13,6 +16,13 @@ public static class MediaTypes
1316

1417
public static class Headers
1518
{
19+
public static int MinimumExpectedVersion = (from fieldInfo in typeof(ExpectedVersion)
20+
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
21+
where fieldInfo.IsLiteral
22+
&& !fieldInfo.IsInitOnly
23+
&& fieldInfo.FieldType == typeof(int)
24+
select (int) fieldInfo.GetRawConstantValue()).Min();
25+
1626
public const string ExpectedVersion = "SSS-ExpectedVersion";
1727
public const string HeadPosition = "SSS-HeadPosition";
1828
public const string Location = "Location";

src/SqlStreamStore.HAL/Streams/StreamResource.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ public async Task<Response> Post(
2929
AppendStreamOperation operation,
3030
CancellationToken cancellationToken)
3131
{
32+
if(operation.ExpectedVersion < Constants.Headers.MinimumExpectedVersion)
33+
{
34+
return new HalJsonResponse(new HALResponse(new
35+
{
36+
type = typeof(WrongExpectedVersionException).Name,
37+
title = "Wrong expected version.",
38+
detail = $"Expected header '{Constants.Headers.ExpectedVersion}' to have an expected version => {ExpectedVersion.NoStream}."
39+
}), 400);
40+
}
41+
3242
var result = await operation.Invoke(_streamStore, cancellationToken);
3343

3444
var links = Links
@@ -40,10 +50,10 @@ public async Task<Response> Post(
4050
var response = new HalJsonResponse(
4151
new HALResponse(result)
4252
.AddLinks(links),
43-
result.CurrentVersion == 0
53+
operation.ExpectedVersion == ExpectedVersion.NoStream
4454
? 201
4555
: 200);
46-
if(operation.ExpectedVersion == ExpectedVersion.NoStream)
56+
if(response.StatusCode == 201)
4757
{
4858
response.Headers[Constants.Headers.Location] =
4959
new[] { $"{_relativePathToRoot}streams/{operation.StreamId}" };

0 commit comments

Comments
 (0)