Skip to content

Commit b9646cf

Browse files
authored
Merge pull request #3 from mberrishdev/unit-tests
Add CI workflow and unit tests for HubDocs functionality
2 parents e15c174 + 1525785 commit b9646cf

4 files changed

Lines changed: 400 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: CI Build and Test
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- master
8+
pull_request:
9+
branches:
10+
- main
11+
- master
12+
13+
jobs:
14+
build-and-test:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Setup .NET
22+
uses: actions/setup-dotnet@v4
23+
with:
24+
dotnet-version: 10.0.x
25+
26+
- name: Restore
27+
run: dotnet restore HubDocs.sln
28+
29+
- name: Build
30+
run: dotnet build HubDocs.sln -c Release --no-restore
31+
32+
- name: Run unit tests
33+
run: dotnet test tests/HubDocs.UnitTests/HubDocs.UnitTests.csproj -c Release --no-build --collect:"XPlat Code Coverage"
34+
35+
- name: Install ReportGenerator
36+
if: always()
37+
run: |
38+
dotnet tool install --global dotnet-reportgenerator-globaltool
39+
echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
40+
41+
- name: Generate HTML coverage report
42+
if: always()
43+
run: |
44+
reportgenerator \
45+
-reports:"tests/HubDocs.UnitTests/TestResults/**/coverage.cobertura.xml" \
46+
-targetdir:"coverage-report" \
47+
-reporttypes:"Html;MarkdownSummary"
48+
49+
- name: Upload coverage artifacts
50+
if: always()
51+
uses: actions/upload-artifact@v4
52+
with:
53+
name: test-coverage-cobertura
54+
path: tests/HubDocs.UnitTests/TestResults/**/coverage.cobertura.xml
55+
56+
- name: Upload HTML coverage report
57+
if: always()
58+
uses: actions/upload-artifact@v4
59+
with:
60+
name: test-coverage-html
61+
path: coverage-report
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
using System.Reflection;
2+
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.AspNetCore.Routing;
4+
using Microsoft.AspNetCore.SignalR;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace HubDocs.UnitTests;
8+
9+
public class ExtensionsInternalTests
10+
{
11+
[Fact]
12+
public async Task AddHubDocs_WhenCalled_ShouldRegisterExpectedRoutes()
13+
{
14+
// Arrange
15+
var builder = WebApplication.CreateBuilder();
16+
builder.Services.AddSignalR();
17+
var app = builder.Build();
18+
19+
// Act
20+
app.AddHubDocs();
21+
await app.StartAsync();
22+
23+
// Assert
24+
var routeEndpoints = app.Services
25+
.GetRequiredService<EndpointDataSource>()
26+
.Endpoints
27+
.OfType<RouteEndpoint>()
28+
.Select(e => e.RoutePattern.RawText)
29+
.ToList();
30+
31+
Assert.Contains("/hubdocs/hubdocs.json", routeEndpoints);
32+
Assert.Contains("/hubdocs/index.html", routeEndpoints);
33+
Assert.Contains("/hubdocs", routeEndpoints);
34+
35+
await app.StopAsync();
36+
}
37+
38+
[Fact]
39+
public async Task GetHubRoutesFromEndpoints_WhenHubMapped_ShouldReturnRoute()
40+
{
41+
// Arrange
42+
var builder = WebApplication.CreateBuilder();
43+
builder.Services.AddSignalR();
44+
var app = builder.Build();
45+
app.MapHub<SimpleHub>("/hubs/simple");
46+
47+
var method = typeof(Extensions).GetMethod(
48+
"GetHubRoutesFromEndpoints",
49+
BindingFlags.NonPublic | BindingFlags.Static);
50+
51+
Assert.NotNull(method);
52+
53+
// Act
54+
await app.StartAsync();
55+
var result = method!.Invoke(null, new object[] { app });
56+
var routes = Assert.IsAssignableFrom<Dictionary<Type, string>>(result);
57+
await app.StopAsync();
58+
59+
// Assert
60+
Assert.Equal("/hubs/simple", routes[typeof(SimpleHub)]);
61+
}
62+
63+
[Fact]
64+
public void DiscoverSignalRHubs_WhenStronglyTypedHubPresent_ShouldIncludeClientMethods()
65+
{
66+
// Arrange
67+
var method = typeof(Extensions).GetMethod(
68+
"DiscoverSignalRHubs",
69+
BindingFlags.NonPublic | BindingFlags.Static);
70+
71+
Assert.NotNull(method);
72+
73+
var routes = new Dictionary<Type, string>
74+
{
75+
{ typeof(TypedHub), "/hubs/typed" },
76+
{ typeof(UnattributedHub), "/hubs/unattributed" }
77+
};
78+
79+
// Act
80+
var result = method!.Invoke(null, new object[] { routes, new[] { typeof(TypedHub).Assembly } });
81+
var discovered = Assert.IsAssignableFrom<IEnumerable<HubMetadata>>(result).ToList();
82+
83+
// Assert
84+
var typed = Assert.Single(discovered, h => h.HubName == nameof(TypedHub));
85+
Assert.Equal("/hubs/typed", typed.Path);
86+
Assert.Equal(typeof(ITypedClient).FullName, typed.ClientInterfaceName);
87+
Assert.NotNull(typed.ClientMethods);
88+
Assert.Contains(typed.ClientMethods!, m => m.MethodName == nameof(ITypedClient.Notify));
89+
Assert.Contains(typed.Methods, m => m.MethodName == nameof(TypedHub.Send));
90+
Assert.DoesNotContain(discovered, h => h.HubName == nameof(UnattributedHub));
91+
Assert.DoesNotContain(discovered, h => h.HubName == nameof(NotRegisteredHub));
92+
}
93+
94+
[Fact]
95+
public void GetMethodSignature_WhenMethodHasParameters_ShouldIncludeParameterTypeNames()
96+
{
97+
// Arrange
98+
var method = typeof(Extensions).GetMethod(
99+
"GetMethodSignature",
100+
BindingFlags.NonPublic | BindingFlags.Static);
101+
var targetMethod = typeof(SignatureHolder).GetMethod(nameof(SignatureHolder.Work));
102+
103+
Assert.NotNull(method);
104+
Assert.NotNull(targetMethod);
105+
106+
// Act
107+
var signature = Assert.IsType<string>(method!.Invoke(null, new object[] { targetMethod! }));
108+
109+
// Assert
110+
Assert.Equal("Work(System.Int32,System.String)", signature);
111+
}
112+
113+
[Fact]
114+
public void FormatType_WhenTypeIsGeneric_ShouldReturnReadableGenericName()
115+
{
116+
// Arrange
117+
var method = typeof(Extensions).GetMethod(
118+
"FormatType",
119+
BindingFlags.NonPublic | BindingFlags.Static);
120+
121+
Assert.NotNull(method);
122+
123+
// Act
124+
var formatted = Assert.IsType<string>(
125+
method!.Invoke(null, new object[] { typeof(Dictionary<string, List<int?>>) }));
126+
127+
// Assert
128+
Assert.Equal("Dictionary<String, List<Nullable<Int32>>>", formatted);
129+
}
130+
131+
[Fact]
132+
public void FormatParameter_WhenParameterIsNullableValueType_ShouldAppendQuestionMark()
133+
{
134+
// Arrange
135+
var method = typeof(Extensions).GetMethod(
136+
"FormatParameter",
137+
BindingFlags.NonPublic | BindingFlags.Static);
138+
139+
var targetMethod = typeof(NullableHolder).GetMethod(nameof(NullableHolder.AcceptsNullableValue));
140+
var parameter = targetMethod!.GetParameters().Single();
141+
142+
Assert.NotNull(method);
143+
144+
// Act
145+
var formatted = Assert.IsType<string>(method!.Invoke(null, new object[] { parameter }));
146+
147+
// Assert
148+
Assert.Equal("Nullable<Int32>?", formatted);
149+
}
150+
151+
[Fact]
152+
public void IsNullable_WhenParameterIsNullableValueType_ShouldReturnTrue()
153+
{
154+
// Arrange
155+
var method = typeof(Extensions).GetMethod(
156+
"IsNullable",
157+
BindingFlags.NonPublic | BindingFlags.Static);
158+
159+
var targetMethod = typeof(NullableHolder).GetMethod(nameof(NullableHolder.AcceptsNullableValue));
160+
var parameter = targetMethod!.GetParameters().Single();
161+
162+
Assert.NotNull(method);
163+
164+
// Act
165+
var result = Assert.IsType<bool>(method!.Invoke(null, new object[] { parameter }));
166+
167+
// Assert
168+
Assert.True(result);
169+
}
170+
171+
[Fact]
172+
public void IsNullable_WhenParameterIsNonNullableReferenceType_ShouldReturnFalse()
173+
{
174+
// Arrange
175+
var method = typeof(Extensions).GetMethod(
176+
"IsNullable",
177+
BindingFlags.NonPublic | BindingFlags.Static);
178+
179+
var targetMethod = typeof(NullableHolder).GetMethod(nameof(NullableHolder.AcceptsNonNullableRef));
180+
var parameter = targetMethod!.GetParameters().Single();
181+
182+
Assert.NotNull(method);
183+
184+
// Act
185+
var result = Assert.IsType<bool>(method!.Invoke(null, new object[] { parameter }));
186+
187+
// Assert
188+
Assert.False(result);
189+
}
190+
191+
[Fact]
192+
public void GetAllPublicHubMethods_WhenDerivedOverridesBaseByName_ShouldExcludeBaseMethodWithSameName()
193+
{
194+
// Arrange
195+
var method = typeof(Extensions).GetMethod(
196+
"GetAllPublicHubMethods",
197+
BindingFlags.NonPublic | BindingFlags.Static);
198+
199+
Assert.NotNull(method);
200+
201+
// Act
202+
var result = method!.Invoke(null, new object[] { typeof(DerivedHub) });
203+
var methods = Assert.IsAssignableFrom<IEnumerable<MethodInfo>>(result).ToList();
204+
205+
// Assert
206+
Assert.Contains(methods, m => m.Name == nameof(DerivedHub.DerivedOnly));
207+
Assert.Contains(methods, m => m.Name == nameof(BaseHub.BaseOnly));
208+
Assert.Single(methods, m => m.Name == nameof(BaseHub.SharedName));
209+
}
210+
211+
public class SignatureHolder
212+
{
213+
public void Work(int a, string b)
214+
{
215+
}
216+
}
217+
218+
public class NullableHolder
219+
{
220+
public void AcceptsNullableRef(string? value)
221+
{
222+
}
223+
224+
public void AcceptsNonNullableRef(string value)
225+
{
226+
}
227+
228+
public void AcceptsNullableValue(int? value)
229+
{
230+
}
231+
}
232+
233+
public class BaseHub : Hub
234+
{
235+
public virtual Task SharedName(string data) => Task.CompletedTask;
236+
237+
public Task BaseOnly() => Task.CompletedTask;
238+
}
239+
240+
public class DerivedHub : BaseHub
241+
{
242+
public override Task SharedName(string data) => Task.CompletedTask;
243+
244+
public Task DerivedOnly() => Task.CompletedTask;
245+
}
246+
247+
public interface ITypedClient
248+
{
249+
Task Notify(string message);
250+
}
251+
252+
[HubDocs]
253+
public class TypedHub : Hub<ITypedClient>
254+
{
255+
public Task Send(string? message, int? code) => Task.CompletedTask;
256+
}
257+
258+
public class UnattributedHub : Hub
259+
{
260+
public Task Ping() => Task.CompletedTask;
261+
}
262+
263+
[HubDocs]
264+
public class NotRegisteredHub : Hub
265+
{
266+
public Task MissingRoute() => Task.CompletedTask;
267+
}
268+
269+
public class SimpleHub : Hub
270+
{
271+
}
272+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace HubDocs.UnitTests;
2+
3+
public class HubDocsAttributeTests
4+
{
5+
[Fact]
6+
public void HubDocsAttribute_WhenQueried_ShouldHaveExpectedUsageMetadata()
7+
{
8+
// Arrange
9+
var usage = (AttributeUsageAttribute?)Attribute.GetCustomAttribute(
10+
typeof(HubDocsAttribute),
11+
typeof(AttributeUsageAttribute));
12+
13+
// Assert
14+
Assert.NotNull(usage);
15+
Assert.Equal(AttributeTargets.Class, usage!.ValidOn);
16+
Assert.False(usage.AllowMultiple);
17+
Assert.False(usage.Inherited);
18+
}
19+
20+
[Fact]
21+
public void HubDocsAttribute_WhenCreated_ShouldInstantiateSuccessfully()
22+
{
23+
// Act
24+
var attribute = new HubDocsAttribute();
25+
26+
// Assert
27+
Assert.NotNull(attribute);
28+
}
29+
}

0 commit comments

Comments
 (0)