@juangcarmona/lighthouse 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/assets/dotnet-template/Directory.Build.props +22 -0
- package/assets/dotnet-template/Directory.Packages.props +16 -0
- package/assets/dotnet-template/Lighthouse.Analyzers/BackendContractAnalyzer.cs +72 -0
- package/assets/dotnet-template/Lighthouse.Analyzers/Lighthouse.Analyzers.csproj +12 -0
- package/assets/dotnet-template/Lighthouse.Analyzers/packages.lock.json +121 -0
- package/assets/dotnet-template/Lighthouse.Cqrs/Handlers.cs +11 -0
- package/assets/dotnet-template/Lighthouse.Cqrs/Lighthouse.Cqrs.csproj +5 -0
- package/assets/dotnet-template/Lighthouse.Cqrs/packages.lock.json +6 -0
- package/assets/dotnet-template/Lighthouse.slnx +17 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/CatalogModule.cs +24 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/CreateEntry.cs +47 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/EntryStore.cs +17 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/GetEntry.cs +30 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Lighthouse.Example.Modules.Catalog.csproj +12 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/packages.lock.json +10 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Lighthouse.Example.Api.csproj +6 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Program.cs +25 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Properties/launchSettings.json +10 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/packages.lock.json +86 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Lighthouse.Example.AppHost.csproj +13 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Program.cs +3 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Properties/launchSettings.json +15 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/aspire.config.json +5 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/packages.lock.json +689 -0
- package/assets/dotnet-template/global.json +6 -0
- package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/BackendContractTests.cs +102 -0
- package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/Lighthouse.Analyzers.Tests.csproj +9 -0
- package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/packages.lock.json +125 -0
- package/assets/dotnet-template/tests/Lighthouse.Example.Tests/CatalogTests.cs +111 -0
- package/assets/dotnet-template/tests/Lighthouse.Example.Tests/Lighthouse.Example.Tests.csproj +9 -0
- package/assets/dotnet-template/tests/Lighthouse.Example.Tests/packages.lock.json +490 -0
- package/assets/guardrails/catalog.json +10 -0
- package/assets/profiles/catalog.json +48 -0
- package/assets/profiles/dotnet/README.md +32 -0
- package/assets/profiles/dotnet/SKILL.md +18 -0
- package/assets/profiles/dotnet/adoption.md +39 -0
- package/assets/profiles/dotnet/architecture.md +37 -0
- package/assets/profiles/dotnet/guardrails.md +32 -0
- package/assets/profiles/dotnet/practices.md +36 -0
- package/assets/profiles/dotnet/testing-and-development.md +29 -0
- package/assets/profiles/dotnet/use-cases.md +29 -0
- package/assets/schemas/lighthouse-manifest.schema.json +11 -7
- package/assets/schemas/profile-catalog.schema.json +63 -0
- package/package.json +6 -2
- package/src/create-dotnet.mjs +112 -0
- package/src/guardrails.mjs +17 -2
- package/src/lighthouse.mjs +156 -24
- package/src/profiles.mjs +133 -0
- package/src/verify-dotnet.mjs +85 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
using System.Collections.Immutable;
|
|
2
|
+
using JuanGCarmona.Lighthouse.Analyzers;
|
|
3
|
+
using Microsoft.CodeAnalysis;
|
|
4
|
+
using Microsoft.CodeAnalysis.CSharp;
|
|
5
|
+
using Microsoft.CodeAnalysis.Diagnostics;
|
|
6
|
+
using Microsoft.CodeAnalysis.Text;
|
|
7
|
+
using Xunit;
|
|
8
|
+
|
|
9
|
+
namespace JuanGCarmona.Lighthouse.Tests;
|
|
10
|
+
|
|
11
|
+
public class BackendContractTests
|
|
12
|
+
{
|
|
13
|
+
private const string Contracts = """
|
|
14
|
+
namespace JuanGCarmona.Lighthouse.Cqrs {
|
|
15
|
+
public interface ICommandHandler<TCommand, TResponse> { }
|
|
16
|
+
public interface IQueryHandler<TQuery, TResponse> { }
|
|
17
|
+
}
|
|
18
|
+
""";
|
|
19
|
+
private static readonly ImmutableArray<MetadataReference> References =
|
|
20
|
+
((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator)
|
|
21
|
+
.Select(p => (MetadataReference)MetadataReference.CreateFromFile(p)).ToImmutableArray();
|
|
22
|
+
|
|
23
|
+
[Theory]
|
|
24
|
+
[InlineData("public sealed class Save : JuanGCarmona.Lighthouse.Cqrs.ICommandHandler<int, int> {}")]
|
|
25
|
+
[InlineData("internal class Save : JuanGCarmona.Lighthouse.Cqrs.ICommandHandler<int, int> {}")]
|
|
26
|
+
[InlineData("public sealed record Read : JuanGCarmona.Lighthouse.Cqrs.IQueryHandler<int, int>;")]
|
|
27
|
+
public async Task HandlerContractReportsVisibilityOrInheritanceExposure(string declaration)
|
|
28
|
+
{
|
|
29
|
+
var diagnostics = await Analyze(Compile(Contracts + declaration));
|
|
30
|
+
Assert.Single(diagnostics, d => d.Id == "LH1001" && d.Severity == DiagnosticSeverity.Error);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[Fact]
|
|
34
|
+
public async Task InternalSealedHandlerPassesAndANameAloneDoesNotTriggerARule()
|
|
35
|
+
{
|
|
36
|
+
var source = Contracts + """
|
|
37
|
+
internal sealed class Save : JuanGCarmona.Lighthouse.Cqrs.ICommandHandler<int, int> {}
|
|
38
|
+
public class PresentationHandler {}
|
|
39
|
+
""";
|
|
40
|
+
Assert.Empty(await Analyze(Compile(source)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[Fact]
|
|
44
|
+
public async Task GeneratedHandlerIsIgnored()
|
|
45
|
+
{
|
|
46
|
+
Assert.Empty(await Analyze(Compile("// <auto-generated/>\n" + Contracts +
|
|
47
|
+
"public class Generated : JuanGCarmona.Lighthouse.Cqrs.ICommandHandler<int, int> {}")));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[Fact]
|
|
51
|
+
public async Task RuleIsOptInForModuleProjects()
|
|
52
|
+
{
|
|
53
|
+
var source = Contracts + "public class Save : JuanGCarmona.Lighthouse.Cqrs.ICommandHandler<int, int> {}";
|
|
54
|
+
Assert.Empty(await Analyze(Compile(source), enabled: false));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[Theory]
|
|
58
|
+
[InlineData("JuanGCarmona.Fixture.Modules.Other", true)]
|
|
59
|
+
[InlineData("JuanGCarmona.Fixture.Modules.Other.Contracts", false)]
|
|
60
|
+
[InlineData("SomeLibrary.Modules.Other", false)]
|
|
61
|
+
public async Task ModuleRuleDistinguishesImplementationContractAndUnrelatedLibrary(string dependency, bool forbidden)
|
|
62
|
+
{
|
|
63
|
+
var assembly = Compile("public sealed class Export {}", dependency);
|
|
64
|
+
using var stream = new MemoryStream();
|
|
65
|
+
var emitted = assembly.Emit(stream);
|
|
66
|
+
Assert.True(emitted.Success, string.Join("\n", emitted.Diagnostics));
|
|
67
|
+
var reference = MetadataReference.CreateFromImage(stream.ToArray());
|
|
68
|
+
var source = Compile("internal sealed class Local {}", "JuanGCarmona.Fixture.Modules.Main")
|
|
69
|
+
.AddReferences(reference);
|
|
70
|
+
var diagnostics = await Analyze(source);
|
|
71
|
+
Assert.Equal(forbidden ? 1 : 0, diagnostics.Count(d => d.Id == "LH1002"));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static CSharpCompilation Compile(string source, string name = "JuanGCarmona.Fixture.Modules.Main") =>
|
|
75
|
+
CSharpCompilation.Create(name, [CSharpSyntaxTree.ParseText(source)], References,
|
|
76
|
+
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
|
77
|
+
|
|
78
|
+
private static async Task<ImmutableArray<Diagnostic>> Analyze(CSharpCompilation compilation, bool enabled = true)
|
|
79
|
+
{
|
|
80
|
+
Assert.Empty(compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error));
|
|
81
|
+
var options = new AnalyzerOptions(ImmutableArray<AdditionalText>.Empty, new ModuleOptions(enabled));
|
|
82
|
+
return await compilation.WithAnalyzers(
|
|
83
|
+
ImmutableArray.Create<DiagnosticAnalyzer>(new BackendContractAnalyzer()), options)
|
|
84
|
+
.GetAnalyzerDiagnosticsAsync();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private sealed class ModuleOptions(bool enabled) : AnalyzerConfigOptionsProvider
|
|
88
|
+
{
|
|
89
|
+
public override AnalyzerConfigOptions GlobalOptions { get; } = new Values(enabled);
|
|
90
|
+
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => GlobalOptions;
|
|
91
|
+
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => GlobalOptions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private sealed class Values(bool enabled) : AnalyzerConfigOptions
|
|
95
|
+
{
|
|
96
|
+
public override bool TryGetValue(string key, out string value)
|
|
97
|
+
{
|
|
98
|
+
value = enabled ? "true" : "false";
|
|
99
|
+
return key == "build_property.LighthouseModule";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/Lighthouse.Analyzers.Tests.csproj
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
<ItemGroup>
|
|
3
|
+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
|
4
|
+
<PackageReference Include="xunit" />
|
|
5
|
+
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
|
|
6
|
+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
|
7
|
+
<ProjectReference Include="../../Lighthouse.Analyzers/Lighthouse.Analyzers.csproj" />
|
|
8
|
+
</ItemGroup>
|
|
9
|
+
</Project>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"dependencies": {
|
|
4
|
+
"net10.0": {
|
|
5
|
+
"Microsoft.CodeAnalysis.CSharp": {
|
|
6
|
+
"type": "Direct",
|
|
7
|
+
"requested": "[5.0.0, )",
|
|
8
|
+
"resolved": "5.0.0",
|
|
9
|
+
"contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"Microsoft.CodeAnalysis.Analyzers": "3.11.0",
|
|
12
|
+
"Microsoft.CodeAnalysis.Common": "[5.0.0]"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"Microsoft.NET.Test.Sdk": {
|
|
16
|
+
"type": "Direct",
|
|
17
|
+
"requested": "[18.7.0, )",
|
|
18
|
+
"resolved": "18.7.0",
|
|
19
|
+
"contentHash": "49xH9j4UzCh2hMohJp53g3wUTvyycECw7CtVht4gfCz5ykudB1uBcF6D0TtgJPjCtP76UPW53bQElKdCeX+dUg==",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"Microsoft.CodeCoverage": "18.7.0",
|
|
22
|
+
"Microsoft.TestPlatform.TestHost": "18.7.0"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"xunit": {
|
|
26
|
+
"type": "Direct",
|
|
27
|
+
"requested": "[2.9.3, )",
|
|
28
|
+
"resolved": "2.9.3",
|
|
29
|
+
"contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"xunit.analyzers": "1.18.0",
|
|
32
|
+
"xunit.assert": "2.9.3",
|
|
33
|
+
"xunit.core": "[2.9.3]"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"xunit.runner.visualstudio": {
|
|
37
|
+
"type": "Direct",
|
|
38
|
+
"requested": "[3.1.5, )",
|
|
39
|
+
"resolved": "3.1.5",
|
|
40
|
+
"contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA=="
|
|
41
|
+
},
|
|
42
|
+
"Microsoft.CodeAnalysis.Common": {
|
|
43
|
+
"type": "Transitive",
|
|
44
|
+
"resolved": "5.0.0",
|
|
45
|
+
"contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"Microsoft.CodeAnalysis.Analyzers": "3.11.0"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"Microsoft.CodeCoverage": {
|
|
51
|
+
"type": "Transitive",
|
|
52
|
+
"resolved": "18.7.0",
|
|
53
|
+
"contentHash": "+wFfx9s7D9wegM0RziXMj2kvYDT4qcqXXtyjiQwSZOGQ2wwcOAJQcD6eQXk02jt0MvRNawtp8TJxTrV+wD8X1g=="
|
|
54
|
+
},
|
|
55
|
+
"Microsoft.TestPlatform.ObjectModel": {
|
|
56
|
+
"type": "Transitive",
|
|
57
|
+
"resolved": "18.7.0",
|
|
58
|
+
"contentHash": "6rmgU4q3/WOpOPcncI0YW0Q/QpcQtwR2TTEXDR5+4TfSimPBAk6Z/BgKLeGgp1SOun0ROVUCCafXhRLwsHaPpA=="
|
|
59
|
+
},
|
|
60
|
+
"Microsoft.TestPlatform.TestHost": {
|
|
61
|
+
"type": "Transitive",
|
|
62
|
+
"resolved": "18.7.0",
|
|
63
|
+
"contentHash": "kYwfmebCs8992zaxEDkvG7S+YEouTeKfYVKUFEkwh1W2dIoOaevBt80XSKVXCUFEhusjOIm1sFfHBnoJgygrRA==",
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"Microsoft.TestPlatform.ObjectModel": "18.7.0",
|
|
66
|
+
"Newtonsoft.Json": "13.0.3"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"Newtonsoft.Json": {
|
|
70
|
+
"type": "Transitive",
|
|
71
|
+
"resolved": "13.0.3",
|
|
72
|
+
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
|
73
|
+
},
|
|
74
|
+
"xunit.abstractions": {
|
|
75
|
+
"type": "Transitive",
|
|
76
|
+
"resolved": "2.0.3",
|
|
77
|
+
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
|
|
78
|
+
},
|
|
79
|
+
"xunit.analyzers": {
|
|
80
|
+
"type": "Transitive",
|
|
81
|
+
"resolved": "1.18.0",
|
|
82
|
+
"contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ=="
|
|
83
|
+
},
|
|
84
|
+
"xunit.assert": {
|
|
85
|
+
"type": "Transitive",
|
|
86
|
+
"resolved": "2.9.3",
|
|
87
|
+
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
|
|
88
|
+
},
|
|
89
|
+
"xunit.core": {
|
|
90
|
+
"type": "Transitive",
|
|
91
|
+
"resolved": "2.9.3",
|
|
92
|
+
"contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
|
|
93
|
+
"dependencies": {
|
|
94
|
+
"xunit.extensibility.core": "[2.9.3]",
|
|
95
|
+
"xunit.extensibility.execution": "[2.9.3]"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"xunit.extensibility.core": {
|
|
99
|
+
"type": "Transitive",
|
|
100
|
+
"resolved": "2.9.3",
|
|
101
|
+
"contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
|
|
102
|
+
"dependencies": {
|
|
103
|
+
"xunit.abstractions": "2.0.3"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"xunit.extensibility.execution": {
|
|
107
|
+
"type": "Transitive",
|
|
108
|
+
"resolved": "2.9.3",
|
|
109
|
+
"contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
|
|
110
|
+
"dependencies": {
|
|
111
|
+
"xunit.extensibility.core": "[2.9.3]"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"JuanGCarmona.Lighthouse.Analyzers": {
|
|
115
|
+
"type": "Project"
|
|
116
|
+
},
|
|
117
|
+
"Microsoft.CodeAnalysis.Analyzers": {
|
|
118
|
+
"type": "CentralTransitive",
|
|
119
|
+
"requested": "[3.11.0, )",
|
|
120
|
+
"resolved": "3.11.0",
|
|
121
|
+
"contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg=="
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
using System.Net;
|
|
2
|
+
using System.Net.Http.Json;
|
|
3
|
+
using System.Security.Claims;
|
|
4
|
+
using System.Text.Encodings.Web;
|
|
5
|
+
using JuanGCarmona.Lighthouse.Example.Api;
|
|
6
|
+
using JuanGCarmona.Lighthouse.Example.Modules.Catalog;
|
|
7
|
+
using Microsoft.AspNetCore.Authentication;
|
|
8
|
+
using Microsoft.AspNetCore.Hosting;
|
|
9
|
+
using Microsoft.AspNetCore.Mvc.Testing;
|
|
10
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
11
|
+
using Microsoft.Extensions.Logging;
|
|
12
|
+
using Microsoft.Extensions.Options;
|
|
13
|
+
using Xunit;
|
|
14
|
+
|
|
15
|
+
namespace JuanGCarmona.Lighthouse.Tests;
|
|
16
|
+
|
|
17
|
+
public class CatalogTests : IClassFixture<CatalogApplication>
|
|
18
|
+
{
|
|
19
|
+
private readonly CatalogApplication application;
|
|
20
|
+
public CatalogTests(CatalogApplication application) => this.application = application;
|
|
21
|
+
|
|
22
|
+
[Fact]
|
|
23
|
+
public async Task AnonymousWriteIsUnauthorized()
|
|
24
|
+
{
|
|
25
|
+
using var client = application.CreateClient();
|
|
26
|
+
var response = await client.PostAsJsonAsync("/api/entries", new { name = "Socket" });
|
|
27
|
+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[Fact]
|
|
31
|
+
public async Task AuthenticatedReaderCannotWrite()
|
|
32
|
+
{
|
|
33
|
+
using var client = application.CreateClient();
|
|
34
|
+
client.DefaultRequestHeaders.Add("X-Test-Identity", "reader");
|
|
35
|
+
var response = await client.PostAsJsonAsync("/api/entries", new { name = "Socket" });
|
|
36
|
+
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
[Theory]
|
|
40
|
+
[InlineData("")]
|
|
41
|
+
[InlineData(" ")]
|
|
42
|
+
[InlineData(null)]
|
|
43
|
+
public async Task InvalidCommandReturnsValidationProblem(string? name)
|
|
44
|
+
{
|
|
45
|
+
using var client = Writer();
|
|
46
|
+
var response = await client.PostAsJsonAsync("/api/entries", new { name });
|
|
47
|
+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
48
|
+
Assert.Equal("application/problem+json", response.Content.Headers.ContentType?.MediaType);
|
|
49
|
+
Assert.Contains("name", await response.Content.ReadAsStringAsync(), StringComparison.Ordinal);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[Fact]
|
|
53
|
+
public async Task CommandThenQueryReturnsTheCreatedResource()
|
|
54
|
+
{
|
|
55
|
+
using var client = Writer();
|
|
56
|
+
var response = await client.PostAsJsonAsync("/api/entries", new { name = " Socket " });
|
|
57
|
+
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
|
58
|
+
Assert.NotNull(response.Headers.Location);
|
|
59
|
+
var entry = await client.GetFromJsonAsync<EntryResponse>(response.Headers.Location);
|
|
60
|
+
Assert.NotNull(entry);
|
|
61
|
+
Assert.Equal("Socket", entry.Name);
|
|
62
|
+
Assert.NotEqual(Guid.Empty, entry.Id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[Fact]
|
|
66
|
+
public async Task UnknownResourceReturnsNotFound()
|
|
67
|
+
{
|
|
68
|
+
using var client = Writer();
|
|
69
|
+
Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync($"/api/entries/{Guid.NewGuid()}")).StatusCode);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[Fact]
|
|
73
|
+
public void CompiledModuleDoesNotExposeHandlersOrReferenceASiblingImplementation()
|
|
74
|
+
{
|
|
75
|
+
var assembly = typeof(CatalogModule).Assembly;
|
|
76
|
+
Assert.DoesNotContain(assembly.GetExportedTypes(), type => type.GetInterfaces()
|
|
77
|
+
.Any(i => i.Namespace == "JuanGCarmona.Lighthouse.Cqrs"));
|
|
78
|
+
Assert.DoesNotContain(assembly.GetReferencedAssemblies(), reference =>
|
|
79
|
+
reference.Name!.StartsWith("JuanGCarmona.Lighthouse.Example.Modules.", StringComparison.Ordinal)
|
|
80
|
+
&& !reference.Name.EndsWith(".Contracts", StringComparison.Ordinal));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private HttpClient Writer()
|
|
84
|
+
{
|
|
85
|
+
var client = application.CreateClient();
|
|
86
|
+
client.DefaultRequestHeaders.Add("X-Test-Identity", "writer");
|
|
87
|
+
return client;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private sealed record EntryResponse(Guid Id, string Name);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public sealed class CatalogApplication : WebApplicationFactory<Program>
|
|
94
|
+
{
|
|
95
|
+
protected override void ConfigureWebHost(IWebHostBuilder builder) => builder.ConfigureServices(services =>
|
|
96
|
+
services.AddAuthentication("test-only").AddScheme<AuthenticationSchemeOptions, TestIdentity>("test-only", _ => { }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public sealed class TestIdentity(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger,
|
|
100
|
+
UrlEncoder encoder) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
|
101
|
+
{
|
|
102
|
+
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
103
|
+
{
|
|
104
|
+
var identity = Request.Headers["X-Test-Identity"].ToString();
|
|
105
|
+
if (identity is not ("writer" or "reader")) return Task.FromResult(AuthenticateResult.NoResult());
|
|
106
|
+
var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, identity) };
|
|
107
|
+
if (identity == "writer") claims.Add(new Claim("permission", "catalog.write"));
|
|
108
|
+
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name));
|
|
109
|
+
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
<ItemGroup>
|
|
3
|
+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
|
4
|
+
<PackageReference Include="xunit" />
|
|
5
|
+
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
|
|
6
|
+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
|
7
|
+
<ProjectReference Include="../../examples/src/root/Lighthouse.Example.Api/Lighthouse.Example.Api.csproj" />
|
|
8
|
+
</ItemGroup>
|
|
9
|
+
</Project>
|