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

Commit 3f3c0ea

Browse files
testing method not allowed and added options test coverage
1 parent c8a4199 commit 3f3c0ea

9 files changed

Lines changed: 201 additions & 53 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.Reflection;
10+
using System.Threading.Tasks;
11+
using Shouldly;
12+
using Xunit;
13+
14+
public class MethodNotAllowedTests : IDisposable
15+
{
16+
private static readonly ImmutableHashSet<HttpMethod> s_AllMethods
17+
= ImmutableHashSet.Create(
18+
(from property in typeof(HttpMethod).GetProperties(BindingFlags.Public | BindingFlags.Static)
19+
where property.PropertyType == typeof(HttpMethod)
20+
select (HttpMethod) property.GetValue(null)).ToArray());
21+
22+
private readonly SqlStreamStoreHalMiddlewareFixture _fixture;
23+
24+
public MethodNotAllowedTests()
25+
{
26+
_fixture = new SqlStreamStoreHalMiddlewareFixture();
27+
}
28+
29+
private static IEnumerable<(string requestUri, HttpMethod method, HttpMethod[] allowed)> CreateTestCases(
30+
string requestUri,
31+
params HttpMethod[] allowed)
32+
=> s_AllMethods.Except(allowed).Select(method => (requestUri, method, allowed));
33+
34+
public static IEnumerable<object[]> MethodNotAllowedCases()
35+
=> CreateTestCases(
36+
"/",
37+
HttpMethod.Get,
38+
HttpMethod.Head,
39+
HttpMethod.Options)
40+
.Concat(CreateTestCases(
41+
"/stream",
42+
HttpMethod.Get,
43+
HttpMethod.Head,
44+
HttpMethod.Options))
45+
.Concat(CreateTestCases(
46+
"/stream/0",
47+
HttpMethod.Get,
48+
HttpMethod.Head,
49+
HttpMethod.Options))
50+
.Concat(CreateTestCases(
51+
"/streams/a-stream",
52+
HttpMethod.Get,
53+
HttpMethod.Head,
54+
HttpMethod.Options,
55+
HttpMethod.Post,
56+
HttpMethod.Delete))
57+
.Concat(CreateTestCases(
58+
"/streams/a-stream/0",
59+
HttpMethod.Get,
60+
HttpMethod.Head,
61+
HttpMethod.Options,
62+
HttpMethod.Delete))
63+
.Concat(CreateTestCases(
64+
$"/streams/a-stream/{Guid.Empty}",
65+
HttpMethod.Get,
66+
HttpMethod.Head,
67+
HttpMethod.Options,
68+
HttpMethod.Delete))
69+
.Concat(CreateTestCases(
70+
"/streams/a-stream/metadata",
71+
HttpMethod.Get,
72+
HttpMethod.Head,
73+
HttpMethod.Options,
74+
HttpMethod.Post))
75+
.Concat(CreateTestCases(
76+
"/docs/doc",
77+
HttpMethod.Get,
78+
HttpMethod.Head,
79+
HttpMethod.Options))
80+
.Select(testCase => new object[] { testCase.requestUri, testCase.method, testCase.allowed });
81+
82+
83+
[Theory, MemberData(nameof(MethodNotAllowedCases))]
84+
public async Task incorrect_method_not_allowed(string requestUri, HttpMethod method, HttpMethod[] allowed)
85+
{
86+
using(var response = await _fixture.HttpClient.SendAsync(new HttpRequestMessage(method, requestUri)))
87+
{
88+
response.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed);
89+
90+
response.Headers.TryGetValues(Constants.Headers.Allowed, out var allowedHeaders).ShouldBeTrue();
91+
92+
allowedHeaders.ShouldBe(allowed.Select(x => x.Method), true);
93+
}
94+
}
95+
96+
public void Dispose() => _fixture.Dispose();
97+
}
98+
}

src/SqlStreamStore.HAL.Tests/OptionsTests.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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,11 @@ 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+
.ShouldBe(new[] { Constants.Headers.ContentType, Constants.Headers.XRequestedWith, Constants.Headers.Authorization }, true);
77+
response.Headers.GetValues(Constants.Headers.AccessControl.AllowOrigin)
6078
.ShouldBe(new[] { "*" }, true);
61-
response.Headers.GetValues("Access-Control-Allow-Methods")
79+
response.Headers.GetValues(Constants.Headers.AccessControl.AllowMethods)
6280
.ShouldBe(allowedMethods.Select(_ => _.Method), true);
6381
}
6482
}

src/SqlStreamStore.HAL/ApplicationBuilderExtensions.cs

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace SqlStreamStore.HAL
66
using System.Threading.Tasks;
77
using Microsoft.AspNetCore.Builder;
88
using Microsoft.AspNetCore.Http;
9+
using Microsoft.Extensions.Primitives;
910
using MidFunc = System.Func<
1011
Microsoft.AspNetCore.Http.HttpContext,
1112
System.Func<System.Threading.Tasks.Task>,
@@ -20,37 +21,55 @@ public static IApplicationBuilder MapWhen(
2021
Action<IApplicationBuilder> configure) => builder.MapWhen(
2122
context => string.Equals(
2223
context.Request.Method,
23-
method.ToString(),
24+
method.Method,
2425
StringComparison.OrdinalIgnoreCase),
2526
configure);
2627

27-
public static IApplicationBuilder UseAllowedMethods<TResource>(this IApplicationBuilder builder, TResource resource)
28-
where TResource : IResource
28+
public static IApplicationBuilder UseAllowedMethods(this IApplicationBuilder builder, IResource resource)
2929
{
30-
var allowed = ResourceMethods.Discover<TResource>();
30+
var allowed = ResourceMethods.Discover(resource);
31+
32+
var allowedMethodsHeaderValue = allowed.Aggregate(
33+
StringValues.Empty,
34+
(previous, method) => StringValues.Concat(previous, method.Method));
35+
36+
var allowedHeadersHeaderValue = new StringValues(new[]
37+
{
38+
Constants.Headers.ContentType,
39+
Constants.Headers.XRequestedWith,
40+
Constants.Headers.Authorization
41+
});
42+
43+
Task Options(HttpContext context, Func<Task> next)
44+
{
45+
context.Response.Headers.Append(
46+
Constants.Headers.AccessControl.AllowMethods,
47+
allowedMethodsHeaderValue);
48+
context.Response.Headers.Append(
49+
Constants.Headers.AccessControl.AllowHeaders,
50+
allowedHeadersHeaderValue);
51+
context.Response.Headers.Append(
52+
Constants.Headers.AccessControl.AllowOrigin,
53+
"*");
54+
55+
return Task.CompletedTask;
56+
}
3157

3258
Task AllowedMethods(HttpContext context, Func<Task> next)
3359
{
3460
if(!allowed.Contains(new HttpMethod(context.Request.Method)))
3561
{
3662
context.Response.StatusCode = 405;
63+
context.Response.Headers.Add(Constants.Headers.Allowed, allowedMethodsHeaderValue);
3764
return Task.CompletedTask;
3865
}
3966

4067
return next();
4168
}
4269

43-
4470
return builder
45-
.Use(Options(allowed))
71+
.MapWhen(HttpMethod.Options, inner => inner.Use(Options))
4672
.Use(AllowedMethods);
4773
}
48-
49-
private static MidFunc Options(HttpMethod[] allowed) => (context, next) =>
50-
{
51-
context.SetStandardCorsHeaders(allowed);
52-
53-
return Task.CompletedTask;
54-
};
5574
}
5675
}

src/SqlStreamStore.HAL/Constants.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ where fieldInfo.IsLiteral
2323
&& fieldInfo.FieldType == typeof(int)
2424
select (int) fieldInfo.GetRawConstantValue()).Min();
2525

26+
public const string Authorization = "Authorization";
27+
public const string Allowed = "Allowed";
2628
public const string ExpectedVersion = "SSS-ExpectedVersion";
2729
public const string HeadPosition = "SSS-HeadPosition";
2830
public const string Location = "Location";
@@ -31,6 +33,14 @@ where fieldInfo.IsLiteral
3133
public const string CacheControl = "Cache-Control";
3234
public const string ContentType = "Content-Type";
3335
public const string Accept = "Accept";
36+
public const string XRequestedWith = "X-Requested-With";
37+
38+
public static class AccessControl
39+
{
40+
public const string AllowOrigin = "Access-Control-Allow-Origin";
41+
public const string AllowHeaders = "Access-Control-Allow-Headers";
42+
public const string AllowMethods = "Access-Control-Allow-Methods";
43+
}
3444
}
3545

3646
public static class Relations
Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
namespace SqlStreamStore.HAL.Docs
22
{
3+
using System;
4+
using System.Net.Http;
5+
using System.Threading.Tasks;
36
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.Http;
48
using MidFunc = System.Func<
59
Microsoft.AspNetCore.Http.HttpContext,
610
System.Func<System.Threading.Tasks.Task>,
@@ -11,16 +15,31 @@ internal static class DocsMiddleware
1115
{
1216
public static IApplicationBuilder UseDocs(
1317
this IApplicationBuilder builder,
14-
params IResource[] resources)
15-
=> builder.Use(Docs(new DocsResource(resources)));
18+
DocsResource documentation)
19+
=> builder.MapWhen(IsMatch, Configure(documentation));
20+
21+
private static bool IsMatch(HttpContext context)
22+
=> context.Request.Path.IsDocs();
1623

17-
private static MidFunc Docs(DocsResource resource) => (context, next) =>
24+
private static bool IsDocs(this PathString requestPath)
25+
=> requestPath.StartsWithSegments("/docs");
26+
27+
private static Action<IApplicationBuilder> Configure(DocsResource documentation)
1828
{
19-
Response response;
20-
if(!context.Request.Path.StartsWithSegments("/docs", out var rel)
21-
|| (response = resource.Get(rel.Value.Remove(0, 1))) == null)
22-
return next();
23-
return context.WriteResponse(response);
24-
};
29+
Task Docs(HttpContext context, Func<Task> next)
30+
{
31+
Response response;
32+
33+
return !context.Request.Path.StartsWithSegments("/docs", out var rel)
34+
|| (response = documentation.Get(rel.Value.Remove(0, 1))) == null
35+
? next()
36+
: context.WriteResponse(response);
37+
}
38+
39+
return builder => builder
40+
.UseMiddlewareLogging(typeof(DocsMiddleware))
41+
.UseAllowedMethods(documentation)
42+
.MapWhen(HttpMethod.Get, inner => inner.Use(Docs));
43+
}
2544
}
2645
}

src/SqlStreamStore.HAL/Docs/DocsResource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ internal class DocsResource : IResource
88
private readonly IResource[] _resources;
99
public SchemaSet Schema { get; }
1010

11-
public DocsResource(IResource[] resources)
11+
public DocsResource(params IResource[] resources)
1212
{
1313
if(resources == null)
1414
{

src/SqlStreamStore.HAL/HttpContextExtensions.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ namespace SqlStreamStore.HAL
22
{
33
using System;
44
using System.Linq;
5-
using System.Net.Http;
65
using System.Net.Http.Headers;
76
using System.Threading.Tasks;
87
using Microsoft.AspNetCore.Http;
@@ -68,27 +67,6 @@ private static Task WriteResponseInternal(HttpContext context, Response response
6867
return response.WriteBody(context.Response, context.RequestAborted);
6968
}
7069

71-
public static void SetStandardCorsHeaders(this HttpContext context, params HttpMethod[] allowedMethods)
72-
{
73-
if(allowedMethods?.Length > 0)
74-
{
75-
context.Response.Headers.Append(
76-
"Access-Control-Allow-Methods",
77-
Array.ConvertAll(allowedMethods, _ => _.Method));
78-
}
79-
80-
context.Response.Headers.Append(
81-
"Access-Control-Allow-Headers",
82-
new[]
83-
{
84-
"Content-Type",
85-
"X-Requested-With",
86-
"Authorization"
87-
});
88-
89-
context.Response.Headers.Append("Access-Control-Allow-Origin", "*");
90-
}
91-
9270
public static int GetExpectedVersion(this HttpRequest request)
9371
=> int.TryParse(
9472
request.Headers[Constants.Headers.ExpectedVersion],

src/SqlStreamStore.HAL/ResourceMethods.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77

88
internal static class ResourceMethods
99
{
10-
public static HttpMethod[] Discover<TResource>()
11-
where TResource : IResource
10+
public static HttpMethod[] Discover(IResource resource)
1211
{
13-
var httpMethods = typeof(TResource)
12+
var httpMethods = resource.GetType()
1413
.GetMethods(
1514
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
1615
.Where(method => method.ReturnType == typeof(Response) || method.ReturnType == typeof(Task<Response>))

src/SqlStreamStore.HAL/SqlStreamStoreHalMiddleware.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,22 @@ public static IApplicationBuilder UseSqlStreamStoreHal(
8383
var streams = new StreamResource(streamStore);
8484
var streamMetadata = new StreamMetadataResource(streamStore);
8585
var streamMessages = new StreamMessageResource(streamStore);
86+
var documentation = new DocsResource(
87+
index,
88+
allStream,
89+
allStreamMessages,
90+
streams,
91+
streamMessages,
92+
streamMetadata);
8693

8794
s_Log.Info(index.ToString);
8895

8996
return builder
9097
.UseExceptionHandling()
9198
.Use(CaseSensitiveQueryStrings)
9299
.Use(HeadRequests)
93-
.UseDocs(index, allStream, allStreamMessages, streams, streamMessages, streamMetadata)
94100
.Use(AcceptHalJson)
101+
.UseDocs(documentation)
95102
.UseIndex(index)
96103
.UseAllStream(allStream)
97104
.UseAllStreamMessage(allStreamMessages)

0 commit comments

Comments
 (0)