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

Commit 53de0e0

Browse files
Merge pull request #34 from thefringeninja/fix-http
Follow HTTP Specification
2 parents c8a4199 + cc14803 commit 53de0e0

22 files changed

Lines changed: 430 additions & 171 deletions

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Fully RESTful HTTP Server for [SQL Stream Store](https://github.com/SQLStreamSto
44

55
The solution includes example of use LittleHalHost.Example project. The url structure and parameters are described in LittleHalHost.Example/readme.md
66

7+
# Development
8+
9+
- `./build.sh` or `./build.cmd` to run the complete dockerized build.
10+
- `dotnet run --project build/build.csproj` to build without using docker.
11+
To see a list of build targets, `dotnet run --project build/build.csproj -- --list-targets`.
12+
For general help with the build, `dotnet run --project build/build.csproj -- --help`.
13+
- If you are running test from your IDE, you _must_ build with the `GenerateDocumentation` target at least once, otherwise the tests around documentation will fail.
14+
715
# Clients
816

917
- [Official dotnet client](https://github.com/SQLStreamStore/SQLStreamStore)
@@ -15,4 +23,4 @@ Ask questions in the `#sql-stream-store` channel in the [ddd-cqrs-es slack](http
1523

1624
# Licences
1725

18-
Licenced under [MIT](LICENSE).
26+
Licenced under [MIT](LICENSE).

src/SqlStreamStore.HAL.Tests/AcceptHeaderTests.cs

Lines changed: 0 additions & 58 deletions
This file was deleted.

src/SqlStreamStore.HAL.Tests/AllJsonSchemasTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ where s_isSqlStreamStoreSchema.IsMatch(manifestName)
4141
public async Task byte_order_mark_not_present(string manifestName)
4242
{
4343
byte[] firstThreeBytes = new byte[3];
44-
44+
4545
using(var stream = GetStream(manifestName))
4646
{
4747
await stream.ReadAsync(firstThreeBytes);
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
namespace SqlStreamStore.HAL.Tests
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Collections.Immutable;
6+
using System.Linq;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Net.Http.Headers;
10+
using System.Reflection;
11+
using System.Threading.Tasks;
12+
using Shouldly;
13+
using Xunit;
14+
15+
public class ClientErrorTests : IDisposable
16+
{
17+
private static readonly ImmutableHashSet<HttpMethod> s_AllMethods
18+
= ImmutableHashSet.Create(
19+
(from property in typeof(HttpMethod).GetProperties(BindingFlags.Public | BindingFlags.Static)
20+
where property.PropertyType == typeof(HttpMethod)
21+
select (HttpMethod) property.GetValue(null)).ToArray());
22+
23+
private static readonly MediaTypeWithQualityHeaderValue s_HalJson =
24+
MediaTypeWithQualityHeaderValue.Parse(Constants.MediaTypes.HalJson);
25+
26+
private static readonly MediaTypeWithQualityHeaderValue s_TextMarkdown =
27+
MediaTypeWithQualityHeaderValue.Parse(Constants.MediaTypes.TextMarkdown);
28+
29+
private static readonly MediaTypeWithQualityHeaderValue s_TextPlain =
30+
MediaTypeWithQualityHeaderValue.Parse("text/plain");
31+
32+
private static readonly (string requestUri, MediaTypeWithQualityHeaderValue[] notAcceptable, HttpMethod[]
33+
methods)
34+
[] s_ResourceConfigurations =
35+
{
36+
("/", new[] { s_TextMarkdown, s_TextPlain }, new[]
37+
{
38+
HttpMethod.Get,
39+
HttpMethod.Head
40+
}),
41+
("/stream", new[] { s_TextMarkdown, s_TextPlain }, new[]
42+
{
43+
HttpMethod.Get,
44+
HttpMethod.Head
45+
}),
46+
("/streams/a-stream", new[] { s_TextMarkdown, s_TextPlain }, new[]
47+
{
48+
HttpMethod.Get,
49+
HttpMethod.Head,
50+
HttpMethod.Post,
51+
HttpMethod.Delete
52+
}),
53+
("/streams/a-stream/0", new[] { s_TextMarkdown, s_TextPlain }, new[]
54+
{
55+
HttpMethod.Get,
56+
HttpMethod.Head,
57+
HttpMethod.Delete
58+
}),
59+
($"/streams/a-stream/{Guid.Empty}", new[] { s_TextMarkdown, s_TextPlain }, new[]
60+
{
61+
HttpMethod.Get,
62+
HttpMethod.Head,
63+
HttpMethod.Delete
64+
}),
65+
66+
("/streams/a-stream/metadata", new[] { s_TextMarkdown, s_TextPlain }, new[]
67+
{
68+
HttpMethod.Get,
69+
HttpMethod.Head,
70+
HttpMethod.Post
71+
}),
72+
("/docs/doc", new[] { s_HalJson, s_TextPlain }, new[]
73+
{
74+
HttpMethod.Get,
75+
HttpMethod.Head
76+
})
77+
};
78+
79+
80+
private readonly SqlStreamStoreHalMiddlewareFixture _fixture;
81+
82+
public ClientErrorTests()
83+
{
84+
_fixture = new SqlStreamStoreHalMiddlewareFixture();
85+
}
86+
87+
public static IEnumerable<object[]> MethodNotAllowedCases()
88+
=> from _ in s_ResourceConfigurations
89+
let allowed = _.methods.Concat(new[] { HttpMethod.Options }).ToArray() // options are always allowed
90+
from method in s_AllMethods.Except(allowed)
91+
select new object[] { _.requestUri, method, allowed };
92+
93+
[Theory, MemberData(nameof(MethodNotAllowedCases))]
94+
public async Task method_not_allowed(string requestUri, HttpMethod method, HttpMethod[] allowed)
95+
{
96+
using(var response = await _fixture.HttpClient.SendAsync(new HttpRequestMessage(method, requestUri)))
97+
{
98+
response.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed);
99+
100+
response.Headers.TryGetValues(Constants.Headers.Allowed, out var allowedHeaders).ShouldBeTrue();
101+
102+
allowedHeaders.ShouldBe(allowed.Select(x => x.Method), true);
103+
}
104+
}
105+
106+
public static IEnumerable<object[]> NotAcceptableCases()
107+
=> from _ in s_ResourceConfigurations
108+
from method in _.methods.Except(new[] { HttpMethod.Delete }).ToArray() // deletes return 204
109+
from notAcceptable in _.notAcceptable
110+
select new object[] { _.requestUri, method, notAcceptable };
111+
112+
[Theory, MemberData(nameof(NotAcceptableCases))]
113+
public async Task not_acceptable(
114+
string requestUri,
115+
HttpMethod method,
116+
MediaTypeWithQualityHeaderValue mediaType)
117+
{
118+
using(var response = await _fixture.HttpClient.SendAsync(new HttpRequestMessage(method, requestUri)
119+
{
120+
Headers = { Accept = { mediaType } }
121+
}))
122+
{
123+
response.StatusCode.ShouldBe(HttpStatusCode.NotAcceptable);
124+
}
125+
}
126+
127+
public void Dispose() => _fixture.Dispose();
128+
}
129+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
namespace SqlStreamStore.HAL.Tests
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
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 Shouldly;
12+
using SqlStreamStore.HAL.StreamMessage;
13+
using SqlStreamStore.HAL.StreamMetadata;
14+
using SqlStreamStore.HAL.Streams;
15+
using Xunit;
16+
17+
public class DocumentationTests : IDisposable
18+
{
19+
private static readonly MediaTypeWithQualityHeaderValue[] s_MarkdownMediaTypes =
20+
{
21+
new MediaTypeWithQualityHeaderValue(Constants.MediaTypes.TextMarkdown),
22+
new MediaTypeWithQualityHeaderValue(Constants.MediaTypes.TextMarkdown)
23+
{
24+
CharSet = "utf-8"
25+
},
26+
new MediaTypeWithQualityHeaderValue(Constants.MediaTypes.Any)
27+
};
28+
private static readonly IDictionary<string, Type> s_documentedRels = new Dictionary<string, Type>
29+
{
30+
["append"] = typeof(StreamResource),
31+
["delete-stream"] = typeof(StreamResource),
32+
["delete-message"] = typeof(StreamMessageResource),
33+
["metadata"] = typeof(StreamMetadataResource)
34+
};
35+
36+
private readonly SqlStreamStoreHalMiddlewareFixture _fixture;
37+
38+
public DocumentationTests()
39+
{
40+
_fixture = new SqlStreamStoreHalMiddlewareFixture();
41+
}
42+
43+
public static IEnumerable<object[]> DocumentationCases()
44+
=> from pair in s_documentedRels
45+
from mediaType in s_MarkdownMediaTypes
46+
select new object[]
47+
{
48+
pair.Key,
49+
mediaType,
50+
pair.Value.Assembly.GetManifestResourceStream(pair.Value, $"Schema.{pair.Key}.schema.md")
51+
};
52+
53+
[Fact]
54+
public async Task documentation_not_found()
55+
{
56+
using(var response = await _fixture.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"/docs/{Guid.NewGuid()}")
57+
{
58+
Headers = { Accept = { MediaTypeWithQualityHeaderValue.Parse(Constants.MediaTypes.TextMarkdown) } }
59+
}))
60+
{
61+
response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
62+
}
63+
}
64+
65+
[Theory, MemberData(nameof(DocumentationCases))]
66+
public async Task documentation(string rel, MediaTypeWithQualityHeaderValue accept, Stream content)
67+
{
68+
content.ShouldNotBeNull();
69+
70+
using (var expectedDocument = new MemoryStream())
71+
using (var actualDocument = new MemoryStream())
72+
using(var response = await _fixture.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"/docs/{rel}")
73+
{
74+
Headers = { Accept = { accept } }
75+
}))
76+
{
77+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
78+
79+
await content.CopyToAsync(expectedDocument);
80+
81+
await response.Content.CopyToAsync(actualDocument);
82+
83+
expectedDocument.ToArray().ShouldBe(actualDocument.ToArray());
84+
}
85+
}
86+
87+
public void Dispose() => _fixture.Dispose();
88+
}
89+
}

src/SqlStreamStore.HAL.Tests/OptionsTests.cs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static IEnumerable<object[]> OptionsAllowedMethodCases()
2727
"/stream",
2828
new[] { HttpMethod.Get, HttpMethod.Head, HttpMethod.Options }
2929
};
30-
30+
3131
yield return new object[]
3232
{
3333
"/stream/123",
@@ -45,6 +45,24 @@ public static IEnumerable<object[]> OptionsAllowedMethodCases()
4545
"/streams/a-stream/0",
4646
new[] { HttpMethod.Get, HttpMethod.Head, HttpMethod.Delete, HttpMethod.Options }
4747
};
48+
49+
yield return new object[]
50+
{
51+
$"/streams/a-stream/{Guid.Empty}",
52+
new[] { HttpMethod.Get, HttpMethod.Head, HttpMethod.Delete, HttpMethod.Options }
53+
};
54+
55+
yield return new object[]
56+
{
57+
"/streams/a-stream/metadata",
58+
new[] { HttpMethod.Get, HttpMethod.Head, HttpMethod.Post, HttpMethod.Options }
59+
};
60+
61+
yield return new object[]
62+
{
63+
"/docs/doc",
64+
new[] { HttpMethod.Get, HttpMethod.Head, HttpMethod.Options }
65+
};
4866
}
4967

5068
[Theory, MemberData(nameof(OptionsAllowedMethodCases))]
@@ -54,11 +72,20 @@ public async Task options_returns_the_correct_cors_headers(string requestUri, Ht
5472
new HttpRequestMessage(HttpMethod.Options, requestUri)))
5573
{
5674
response.StatusCode.ShouldBe(HttpStatusCode.OK);
57-
response.Headers.GetValues("Access-Control-Allow-Headers")
58-
.ShouldBe(new[] { "Content-Type", "X-Requested-With", "Authorization" }, true);
59-
response.Headers.GetValues("Access-Control-Allow-Origin")
75+
response.Headers.GetValues(Constants.Headers.AccessControl.AllowHeaders)
76+
.SelectMany(x => x.Split(','))
77+
.ShouldBe(new[]
78+
{
79+
Constants.Headers.ContentType,
80+
Constants.Headers.XRequestedWith,
81+
Constants.Headers.Authorization
82+
},
83+
true);
84+
response.Headers.GetValues(Constants.Headers.AccessControl.AllowOrigin)
85+
.SelectMany(x => x.Split(','))
6086
.ShouldBe(new[] { "*" }, true);
61-
response.Headers.GetValues("Access-Control-Allow-Methods")
87+
response.Headers.GetValues(Constants.Headers.AccessControl.AllowMethods)
88+
.SelectMany(x => x.Split(','))
6289
.ShouldBe(allowedMethods.Select(_ => _.Method), true);
6390
}
6491
}

0 commit comments

Comments
 (0)