Skip to content

Commit bcb1c2d

Browse files
Merge pull request #18 from webexpress-framework/develop
Develop 0.0.11-alpha
2 parents 47787a5 + f2775b0 commit bcb1c2d

412 files changed

Lines changed: 7514 additions & 2363 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/generate-docs.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- name: Setup .NET SDK
2929
uses: actions/setup-dotnet@v3
3030
with:
31-
dotnet-version: 9.x
31+
dotnet-version: 10.x
3232

3333
- name: Install DocFX
3434
run: dotnet tool install -g docfx
@@ -49,6 +49,11 @@ jobs:
4949
docfx metadata
5050
docfx build
5151
52+
- name: Validate generated documentation output
53+
run: |
54+
test -f docs/_site/index.html
55+
test -f docs/_site/api/toc.json
56+
5257
- name: Generate API toc.yaml
5358
run: |
5459
echo "### YamlMime:TableOfContent" > docs/api/toc.yaml

.github/workflows/unittest-verification.yml

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,80 @@ jobs:
2323
dotnet-version: 10.x
2424

2525
- name: Restore dependencies
26-
run: dotnet restore
26+
run: dotnet restore ./WebExpress.WebCore.sln
2727

2828
- name: Build project
29-
run: dotnet build --no-restore --configuration Release
29+
run: dotnet build ./WebExpress.WebCore.sln --no-restore --configuration Release
3030

3131
- name: Run xUnit tests
32-
run: dotnet test --no-build --configuration Release --logger "trx"
32+
id: tests
33+
continue-on-error: true
34+
run: dotnet test ./WebExpress.WebCore.Test/WebExpress.WebCore.Test.csproj --no-build --configuration Release -- --report-trx --results-directory ./TestResults
35+
36+
- name: Print failing tests on failure
37+
if: steps.tests.outcome == 'failure'
38+
shell: bash
39+
run: |
40+
python3 - <<'PY'
41+
import glob
42+
import sys
43+
import xml.etree.ElementTree as ET
44+
45+
ns = '{http://microsoft.com/schemas/VisualStudio/TeamTest/2010}'
46+
files = sorted(glob.glob('./TestResults/**/*.trx', recursive=True))
47+
if not files:
48+
print('No TRX report produced - cannot extract failure details.')
49+
sys.exit(0)
50+
51+
total_failed = 0
52+
for path in files:
53+
print(f'::group::Report {path}')
54+
try:
55+
root = ET.parse(path).getroot()
56+
except ET.ParseError as exc:
57+
print(f'Failed to parse {path}: {exc}')
58+
print('::endgroup::')
59+
continue
60+
61+
report_failed = 0
62+
for result in root.iter(f'{ns}UnitTestResult'):
63+
if result.get('outcome') != 'Failed':
64+
continue
65+
report_failed += 1
66+
total_failed += 1
67+
print(f'\n--- FAILED: {result.get("testName")} ---')
68+
69+
err = result.find(f'.//{ns}ErrorInfo')
70+
if err is not None:
71+
message = err.find(f'{ns}Message')
72+
stack = err.find(f'{ns}StackTrace')
73+
if message is not None and message.text:
74+
print('Message:')
75+
print(message.text.rstrip())
76+
if stack is not None and stack.text:
77+
print('StackTrace:')
78+
print(stack.text.rstrip())
79+
80+
stdout = result.find(f'.//{ns}Output/{ns}StdOut')
81+
if stdout is not None and stdout.text:
82+
print('StdOut:')
83+
print(stdout.text.rstrip())
84+
85+
if report_failed == 0:
86+
print('(no failed tests in this report)')
87+
print('::endgroup::')
88+
89+
print(f'\nTotal failed tests: {total_failed}')
90+
PY
91+
92+
- name: Upload TRX report
93+
if: always()
94+
uses: actions/upload-artifact@v4
95+
with:
96+
name: trx-results
97+
path: src/TestResults/**/*.trx
98+
if-no-files-found: ignore
99+
100+
- name: Fail workflow when tests failed
101+
if: steps.tests.outcome == 'failure'
102+
run: exit 1

src/WebExpress.WebCore.Test/Data/MockIdentity.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,27 @@ internal class MockIdentity : IIdentity
1010
private readonly List<IIdentityGroup> _groups = new();
1111

1212
/// <summary>
13-
/// Returns or sets the id of the user.
13+
/// Gets the id of the user.
1414
/// </summary>
1515
public Guid Id { get; }
1616

1717
/// <summary>
18-
/// Returns or sets the name of the user.
18+
/// Gets the name of the user.
1919
/// </summary>
2020
public string Name { get; }
2121

2222
/// <summary>
23-
/// Returns or sets the email of the user.
23+
/// Gets the email of the user.
2424
/// </summary>
2525
public string Email { get; }
2626

2727
/// <summary>
28-
/// Returns the hash of the password.
28+
/// Gets the hash of the password.
2929
/// </summary>
3030
public string PasswordHash { get; }
3131

3232
/// <summary>
33-
/// Returns the groups associated with the user.
33+
/// Gets the groups associated with the user.
3434
/// </summary>
3535
public IEnumerable<IIdentityGroup> Groups => _groups;
3636

src/WebExpress.WebCore.Test/Data/MockIdentityGroup.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ internal class MockIdentityGroup : IIdentityGroup
1010
private readonly List<string> _roles = [];
1111

1212
/// <summary>
13-
/// Returns or sets the id of the group.
13+
/// Gets or sets the id of the group.
1414
/// </summary>
1515
public Guid Id { get; set; }
1616

1717
/// <summary>
18-
/// Returns or sets the name of the group.
18+
/// Gets or sets the name of the group.
1919
/// </summary>
2020
public string Name { get; set; }
2121

2222
/// <summary>
23-
/// Returns the roles associated with the group.
23+
/// Gets the roles associated with the group.
2424
/// </summary>
2525
public IEnumerable<string> Policies => _roles;
2626

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using WebExpress.WebCore.WebIdentity;
2+
using WebExpress.WebCore.WebMessage;
3+
using WebExpress.WebCore.WebPage;
4+
5+
namespace WebExpress.WebCore.Test.Data
6+
{
7+
/// <summary>
8+
/// Provides a mock implementation of the IIdentityProvider interface for testing purposes.
9+
/// </summary>
10+
/// <remarks>
11+
/// This class simulates an identity provider by allowing test code to manage in-memory
12+
/// collections of identities and groups. It is intended for use in unit tests or development scenarios where a real
13+
/// identity provider is not required.
14+
/// </remarks>
15+
public class MockIdentityProvider : IIdentityProvider
16+
{
17+
/// <summary>
18+
/// Gets the collection of identities associated with the current principal.
19+
/// </summary>
20+
public List<IIdentity> Identities { get; } = [];
21+
22+
/// <summary>
23+
/// Gets the collection of identity groups associated with the current user or entity.
24+
/// </summary>
25+
public List<IIdentityGroup> Groups { get; } = [];
26+
27+
/// <summary>
28+
/// Returns a collection of all associated identities for the current principal.
29+
/// </summary>
30+
/// <returns>
31+
/// An enumerable collection of <see cref="IIdentity"/> objects representing the identities associated with the
32+
/// principal. The collection may be empty if no identities are present.
33+
/// </returns>
34+
public IEnumerable<IIdentity> GetIdentities() => Identities;
35+
36+
/// <summary>
37+
/// Retrieves a collection of identity groups associated with the current context.
38+
/// </summary>
39+
/// <returns>
40+
/// An enumerable collection of objects that implement the IIdentityGroup interface. The collection may be empty
41+
/// if no groups are associated.
42+
/// </returns>
43+
public IEnumerable<IIdentityGroup> GetGroups() => Groups;
44+
45+
/// <summary>
46+
/// Authenticates the specified request and returns the associated identity.
47+
/// </summary>
48+
/// <param name="request">
49+
/// The request to authenticate. Cannot be null.
50+
/// </param>
51+
/// <returns>
52+
/// An identity representing the authenticated user if authentication is successful; otherwise, null.
53+
/// </returns>
54+
public IIdentity Authenticate(IRequest request)
55+
{
56+
return null; // not needed for this test
57+
}
58+
59+
/// <summary>
60+
/// Logs out the specified request by clearing any authentication state.
61+
/// </summary>
62+
/// <param name="request">
63+
/// The request whose authentication state should be cleared. Cannot be null.
64+
/// </param>
65+
public void Logout(IRequest request)
66+
{
67+
// not needed for this test
68+
}
69+
70+
/// <summary>
71+
/// Displays a login dialog using the specified request and identity information.
72+
/// </summary>
73+
/// <param name="request">
74+
/// The request containing parameters and context for the login operation. Cannot be null.
75+
/// </param>
76+
/// <param name="initiator">
77+
/// The endpoint that triggered the authentication process. Used to determine the origin and
78+
/// context of the authentication requirement.
79+
/// </param>
80+
/// <param name="identity">
81+
/// The identity information to be used for authentication. Cannot be null.
82+
/// </param>
83+
/// <returns>
84+
/// An object that represents the response to the login dialog, including authentication results and any
85+
/// relevant status information.
86+
/// </returns>
87+
public IResponse CreateAuthenticationPrompt(IRequest request, IPageContext initiator, IIdentity identity)
88+
{
89+
return null;
90+
}
91+
92+
/// <summary>
93+
/// Creates a forbidden response page for the specified request when the authenticated
94+
/// user lacks the required permissions to access the requested resource.
95+
/// </summary>
96+
/// <param name="request">
97+
/// The request for which access was denied. Cannot be null.
98+
/// </param>
99+
/// <param name="initiator">
100+
/// The endpoint that the user attempted to access.
101+
/// </param>
102+
/// <param name="identity">
103+
/// The authenticated identity that lacks sufficient permissions.
104+
/// </param>
105+
/// <returns>
106+
/// A response representing the forbidden page if this provider can handle the forbidden
107+
/// scenario; otherwise, <c>null</c>.
108+
/// </returns>
109+
public IResponse CreateForbiddenPage(IRequest request, IPageContext initiator, IIdentity identity)
110+
{
111+
return null;
112+
}
113+
}
114+
}

src/WebExpress.WebCore.Test/Fixture/UnitTestFixture.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public static ComponentHub CreateComponentHubMock(IHttpServerContext httpServerC
6060
(
6161
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
6262
null,
63-
[typeof(HttpServerContext)],
63+
[typeof(IHttpServerContext)],
6464
null
6565
);
6666

@@ -140,11 +140,35 @@ public static WebMessage.HttpContext CreateHttpContextMock(string content = "")
140140
{
141141
var ctorRequest = typeof(Request).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [typeof(IFeatureCollection), typeof(RequestHeaderFields), typeof(IHttpServerContext)], null);
142142
var featureCollection = new FeatureCollection();
143-
var firstLine = content.Split('\n').FirstOrDefault();
143+
var firstLine = content.Split('\n').FirstOrDefault()?.TrimEnd('\r') ?? "";
144144
var lines = content.Split(_separator, StringSplitOptions.None);
145145
var filteredLines = lines.Skip(1).TakeWhile(line => !string.IsNullOrWhiteSpace(line));
146-
var pos = content.Length > 0 ? content.IndexOf(filteredLines.LastOrDefault() ?? "") + filteredLines.LastOrDefault()?.Length ?? 0 + 4 : 0;
147-
var innerContent = pos < content.Length ? content[pos..] : "";
146+
147+
// locate the headers/body boundary in a line-ending-agnostic way: the
148+
// first occurrence of two consecutive line breaks (any combination of
149+
// \r\n, \n, \r) marks the end of the header section.
150+
var headerEnd = -1;
151+
var separatorLength = 0;
152+
foreach (var sep in new[] { "\r\n\r\n", "\n\n", "\r\r" })
153+
{
154+
var idx = content.IndexOf(sep, StringComparison.Ordinal);
155+
if (idx >= 0 && (headerEnd < 0 || idx < headerEnd))
156+
{
157+
headerEnd = idx;
158+
separatorLength = sep.Length;
159+
}
160+
}
161+
162+
var innerContent = headerEnd >= 0 ? content[(headerEnd + separatorLength)..] : "";
163+
164+
// HTTP wire format requires CRLF; normalize text-only bodies that
165+
// were checked out with LF only so the production multipart /
166+
// urlencoded parsers find their boundaries.
167+
if (innerContent.Length > 0 && !innerContent.Contains("\r\n"))
168+
{
169+
innerContent = innerContent.Replace("\n", "\r\n");
170+
}
171+
148172
var contentBytes = Encoding.UTF8.GetBytes(innerContent);
149173

150174
var requestFeature = new HttpRequestFeature

0 commit comments

Comments
 (0)