Skip to content

Commit 7472eff

Browse files
committed
TEST: Add test project and update package dependencies
1 parent d9033d3 commit 7472eff

File tree

17 files changed

+1547
-2
lines changed

17 files changed

+1547
-2
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ jobs:
3535
- name: 🔨 Build solution
3636
run: dotnet build --configuration Release --no-restore
3737

38+
- name: ✅ Run tests
39+
run: dotnet test --configuration Release --no-build --verbosity normal
40+
3841
- name: 📦 Pack
3942
run: dotnet pack --configuration Release --no-build --output ./nupkg
4043

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131
- name: 🔨 Build
3232
run: dotnet build --configuration Release --no-restore
3333

34+
- name: ✅ Run tests
35+
run: dotnet test --configuration Release --no-build --verbosity normal
36+
3437
- name: 📦 Pack
3538
run: dotnet pack --configuration Release --no-build --output ./nupkg
3639

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using FluentAssertions;
2+
using System;
3+
using Xunit;
4+
5+
namespace DG.XrmPluginCore.Tests
6+
{
7+
public class AbstractPluginTests
8+
{
9+
[Fact]
10+
public void Constructor_ShouldSetChildClassName()
11+
{
12+
// Arrange & Act
13+
var plugin = new TestAbstractPlugin();
14+
15+
// Assert
16+
plugin.GetChildClassName().Should().Be(typeof(TestAbstractPlugin).ToString());
17+
}
18+
19+
[Fact]
20+
public void Execute_IsAbstract_ShouldRequireImplementation()
21+
{
22+
// Arrange
23+
var plugin = new TestAbstractPlugin();
24+
25+
// Act & Assert
26+
// This test verifies that the Execute method is implemented in the derived class
27+
Assert.Throws<NotImplementedException>(() => plugin.Execute(null));
28+
}
29+
}
30+
31+
// Test implementation of AbstractPlugin
32+
public class TestAbstractPlugin : AbstractPlugin
33+
{
34+
public string GetChildClassName() => ChildClassName;
35+
36+
public override void Execute(IServiceProvider serviceProvider)
37+
{
38+
throw new NotImplementedException("Test implementation");
39+
}
40+
}
41+
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
using DG.XrmPluginCore.Enums;
2+
using DG.XrmPluginCore.Tests.Helpers;
3+
using DG.XrmPluginCore.Tests.TestCustomApis;
4+
using FluentAssertions;
5+
using Microsoft.Xrm.Sdk;
6+
using NSubstitute;
7+
using System;
8+
using System.Linq;
9+
using System.ServiceModel;
10+
using Xunit;
11+
12+
namespace DG.XrmPluginCore.Tests
13+
{
14+
public class CustomAPITests
15+
{
16+
[Fact]
17+
public void Execute_NullServiceProvider_ShouldThrowArgumentNullException()
18+
{
19+
// Arrange
20+
var customApi = new TestCustomAPI();
21+
22+
// Act & Assert
23+
Assert.Throws<ArgumentNullException>(() => customApi.Execute(null));
24+
}
25+
26+
[Fact]
27+
public void Execute_ValidRegistration_ShouldExecuteAction()
28+
{
29+
// Arrange
30+
var customApi = new TestCustomAPI();
31+
var mockProvider = new MockServiceProvider();
32+
33+
// Act
34+
customApi.Execute(mockProvider.ServiceProvider);
35+
36+
// Assert
37+
customApi.ExecutedAction.Should().BeTrue();
38+
customApi.LastContext.Should().NotBeNull();
39+
}
40+
41+
[Fact]
42+
public void Execute_NoRegistration_ShouldNotExecuteAction()
43+
{
44+
// Arrange
45+
var customApi = new TestNoRegistrationCustomAPI();
46+
var mockProvider = new MockServiceProvider();
47+
48+
// Act
49+
customApi.Execute(mockProvider.ServiceProvider);
50+
51+
// Assert
52+
customApi.ExecutedAction.Should().BeFalse();
53+
}
54+
55+
[Fact]
56+
public void Execute_FaultException_ShouldRethrow()
57+
{
58+
// Arrange
59+
var mockProvider = new MockServiceProvider();
60+
var customApi = new TestCustomAPI();
61+
62+
// Setup organization service factory to throw exception when creating organization service
63+
var faultException = new FaultException<OrganizationServiceFault>(new OrganizationServiceFault());
64+
mockProvider.OrganizationServiceFactory.CreateOrganizationService(Arg.Any<Guid?>())
65+
.Returns(x => { throw faultException; });
66+
67+
// Act & Assert
68+
var exception = Assert.Throws<FaultException<OrganizationServiceFault>>(() => customApi.Execute(mockProvider.ServiceProvider));
69+
exception.Should().Be(faultException);
70+
}
71+
72+
[Fact]
73+
public void RegisterCustomAPI_MultipleRegistrations_ShouldThrowInvalidOperationException()
74+
{
75+
// Arrange
76+
var customApi = new TestMultipleRegistrationCustomAPI();
77+
78+
// Act & Assert
79+
Assert.Throws<InvalidOperationException>(() => customApi.TryRegisterSecond());
80+
}
81+
82+
[Fact]
83+
public void GetRegistration_ValidRegistration_ShouldReturnConfiguration()
84+
{
85+
// Arrange
86+
var customApi = new TestCustomAPI();
87+
88+
// Act
89+
var registration = customApi.GetRegistration();
90+
91+
// Assert
92+
registration.Should().NotBeNull();
93+
registration.Name.Should().Be("test_custom_api");
94+
registration.UniqueName.Should().Be("test_custom_api");
95+
}
96+
97+
[Fact]
98+
public void GetRegistration_WithConfiguration_ShouldReturnFullConfiguration()
99+
{
100+
// Arrange
101+
var customApi = new TestCustomAPIWithConfig();
102+
103+
// Act
104+
var registration = customApi.GetRegistration();
105+
106+
// Assert
107+
registration.Should().NotBeNull();
108+
registration.Name.Should().Be("test_custom_api_with_config");
109+
registration.Description.Should().Be("Test Custom API");
110+
registration.IsFunction.Should().BeTrue();
111+
registration.IsPrivate.Should().BeTrue();
112+
registration.EnabledForWorkflow.Should().BeTrue();
113+
114+
var requestParams = registration.RequestParameters.ToList();
115+
requestParams.Should().HaveCount(1);
116+
requestParams[0].UniqueName.Should().Be("input_param");
117+
requestParams[0].Type.Should().Be(CustomApiParameterType.String);
118+
119+
var responseProps = registration.ResponseProperties.ToList();
120+
responseProps.Should().HaveCount(1);
121+
responseProps[0].UniqueName.Should().Be("output_prop");
122+
responseProps[0].Type.Should().Be(CustomApiParameterType.String);
123+
}
124+
125+
[Fact]
126+
public void OnBeforeConstructLocalPluginContext_ShouldAllowModification()
127+
{
128+
// Arrange
129+
var customApi = new TestServiceProviderModificationCustomAPI();
130+
var originalProvider = new MockServiceProvider();
131+
132+
// Act
133+
customApi.Execute(originalProvider.ServiceProvider);
134+
135+
// Assert
136+
customApi.ModifiedServiceProviderUsed.Should().BeTrue();
137+
}
138+
139+
[Fact]
140+
public void Execute_ShouldTraceEntryAndExit()
141+
{
142+
// Arrange
143+
var customApi = new TestCustomAPI();
144+
var mockProvider = new MockServiceProvider();
145+
146+
// Act
147+
customApi.Execute(mockProvider.ServiceProvider);
148+
149+
// Assert
150+
mockProvider.TracingService.Received().Trace(
151+
"{0}, Correlation Id: {1}, Initiating User: {2}",
152+
Arg.Is<string>(s => s.Contains("Entered") && s.Contains("Execute")),
153+
Arg.Any<Guid>(),
154+
Arg.Any<Guid>());
155+
mockProvider.TracingService.Received().Trace(
156+
"{0}, Correlation Id: {1}, Initiating User: {2}",
157+
Arg.Is<string>(s => s.Contains("Exiting") && s.Contains("Execute")),
158+
Arg.Any<Guid>(),
159+
Arg.Any<Guid>());
160+
}
161+
162+
[Fact]
163+
public void Execute_ShouldTraceStage()
164+
{
165+
// Arrange
166+
var customApi = new TestCustomAPI();
167+
var mockProvider = new MockServiceProvider();
168+
var stage = 30;
169+
mockProvider.PluginExecutionContext.Stage.Returns(stage);
170+
171+
// Act
172+
customApi.Execute(mockProvider.ServiceProvider);
173+
174+
// Assert
175+
mockProvider.TracingService.Received().Trace(
176+
"{0}, Correlation Id: {1}, Initiating User: {2}",
177+
stage.ToString(),
178+
Arg.Any<Guid>(),
179+
Arg.Any<Guid>());
180+
}
181+
182+
[Fact]
183+
public void Execute_ShouldTraceExecutionInfo()
184+
{
185+
// Arrange
186+
var customApi = new TestCustomAPI();
187+
var mockProvider = new MockServiceProvider();
188+
var entityName = "test_entity";
189+
var messageName = "test_message";
190+
191+
mockProvider.PluginExecutionContext.PrimaryEntityName.Returns(entityName);
192+
mockProvider.PluginExecutionContext.MessageName.Returns(messageName);
193+
194+
// Act
195+
customApi.Execute(mockProvider.ServiceProvider);
196+
197+
// Assert
198+
mockProvider.TracingService.Received().Trace(
199+
"{0}, Correlation Id: {1}, Initiating User: {2}",
200+
Arg.Is<string>(s => s.Contains(entityName) && s.Contains(messageName) && s.Contains("is firing for")),
201+
Arg.Any<Guid>(),
202+
Arg.Any<Guid>());
203+
}
204+
}
205+
206+
// Helper custom API for testing service provider modification
207+
public class TestServiceProviderModificationCustomAPI : CustomAPI
208+
{
209+
public bool ModifiedServiceProviderUsed { get; private set; }
210+
211+
public TestServiceProviderModificationCustomAPI()
212+
{
213+
RegisterCustomAPI("test_modification_api", Execute);
214+
}
215+
216+
protected override IServiceProvider OnBeforeConstructLocalPluginContext(IServiceProvider serviceProvider)
217+
{
218+
// Create a wrapper that marks when it's used
219+
var wrapper = Substitute.For<IServiceProvider>();
220+
wrapper.GetService(Arg.Any<Type>()).Returns(callInfo =>
221+
{
222+
ModifiedServiceProviderUsed = true;
223+
return serviceProvider.GetService(callInfo.Arg<Type>());
224+
});
225+
226+
return wrapper;
227+
}
228+
229+
private void Execute(LocalPluginContext context)
230+
{
231+
// Action implementation
232+
}
233+
}
234+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net462;net8.0</TargetFrameworks>
5+
<IsPackable>false</IsPackable>
6+
<IsTestProject>true</IsTestProject>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
11+
<PackageReference Include="xunit" Version="2.9.3" />
12+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
13+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
14+
<PrivateAssets>all</PrivateAssets>
15+
</PackageReference>
16+
<PackageReference Include="coverlet.collector" Version="6.0.4">
17+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18+
<PrivateAssets>all</PrivateAssets>
19+
</PackageReference>
20+
<PackageReference Include="NSubstitute" Version="5.3.0" />
21+
<PackageReference Include="FluentAssertions" Version="8.6.0" />
22+
</ItemGroup>
23+
24+
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
25+
<PackageReference Include="Microsoft.CrmSdk.CoreAssemblies" Version="9.0.2.59" />
26+
</ItemGroup>
27+
28+
<ItemGroup Condition="'$(TargetFramework)' != 'net462'">
29+
<PackageReference Include="Microsoft.PowerPlatform.Dataverse.Client" Version="1.2.3" />
30+
</ItemGroup>
31+
32+
<ItemGroup>
33+
<ProjectReference Include="..\DG.XrmPluginCore\DG.XrmPluginCore.csproj" />
34+
<ProjectReference Include="..\DG.XrmPluginCore.Abstractions\DG.XrmPluginCore.Abstractions.csproj" />
35+
</ItemGroup>
36+
37+
</Project>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
[assembly: SuppressMessage("Microsoft.Design", "CA1014:MarkAssembliesWithClsCompliant")]
4+
5+
// Global test configuration and shared attributes can be added here

0 commit comments

Comments
 (0)