Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: "CodeQL for C#"

on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop

jobs:
analyze:
name: CodeQL Analyze (C#)
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read
actions: read
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: csharp
build-mode: manual

- name: Build .NET project
run: |
dotnet restore
dotnet build --configuration Release --no-restore

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:csharp"
11 changes: 11 additions & 0 deletions .github/workflows/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: "nuget"
directory: "/" # Location of dotnet solution (*.sln) or project (*.csproj)
schedule:
interval: "weekly"
14 changes: 11 additions & 3 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ name: .NET

on:
push:
branches: [ "main" ]
branches:
- main
- develop
pull_request:
branches: [ "main" ]
branches:
- main
- develop

jobs:
build:
Expand All @@ -16,13 +20,17 @@ jobs:

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore
run: dotnet build

- name: Test
run: dotnet test --no-build --verbosity normal
28 changes: 19 additions & 9 deletions .github/workflows/sonarqube.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,55 @@ on:
push:
branches:
- main
- develop
pull_request:
types: [opened, synchronize, reopened]
jobs:
build:
name: Build and analyze
runs-on: windows-latest
runs-on: ubuntu-latest
steps:
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu' # Alternative distribution options are available.

- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

- name: Setup .NET 9.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- name: Cache SonarQube Cloud packages
uses: actions/cache@v4
with:
path: ~\sonar\cache
path: ~/sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar

- name: Cache SonarQube Cloud scanner
id: cache-sonar-scanner
uses: actions/cache@v4
with:
path: .\.sonar\scanner
path: ./.sonar/scanner
key: ${{ runner.os }}-sonar-scanner
restore-keys: ${{ runner.os }}-sonar-scanner

- name: Install SonarQube Cloud scanner
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
shell: powershell
run: |
New-Item -Path .\.sonar\scanner -ItemType Directory
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
mkdir -p ./.sonar/scanner
dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner

- name: Build and analyze
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: powershell
run: |
.\.sonar\scanner\dotnet-sonarscanner begin /k:"MathMax80_MathMax.Administration" /o:"mathmax80" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io"
./.sonar/scanner/dotnet-sonarscanner begin /k:"MathMax80_MathMax.Administration" /o:"mathmax80" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.scanner.scanAll=false /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml"
dotnet build
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
dotnet test --collect:"XPlat Code Coverage" --results-directory ./TestResults/ --logger trx --configuration Release -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using Xunit;

namespace MathMax.EventSourcing.UnitTests.Commands;

/// <summary>
/// Unit tests for CommandHandlerBase constructor parameter validation.
/// Verifies that the constructor properly validates required dependencies
/// and throws appropriate exceptions when null values are provided.
/// </summary>
public class CommandHandlerBaseConstructorTests : IClassFixture<CommandHandlerBaseTestFixture>
{
private readonly CommandHandlerBaseTestFixture _fixture;

public CommandHandlerBaseConstructorTests(CommandHandlerBaseTestFixture fixture)
{
_fixture = fixture;
}

[Fact]
public void Constructor_WhenEventStoreIsNull_ThrowsArgumentNullExceptionWithCorrectParameterName()
{
// Arrange
var dependencies = _fixture.CreateTestDependencies();

// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
new TestCommandHandler(null!, dependencies.MockSerializer.Object, dependencies.MockDateTimeService.Object));

Assert.Equal("eventStore", exception.ParamName);
}

[Fact]
public void Constructor_WhenSerializerIsNull_ThrowsArgumentNullExceptionWithCorrectParameterName()
{
// Arrange
var dependencies = _fixture.CreateTestDependencies();

// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
new TestCommandHandler(dependencies.MockEventStore.Object, null!, dependencies.MockDateTimeService.Object));

Assert.Equal("serializer", exception.ParamName);
}

[Fact]
public void Constructor_WhenDateTimeServiceIsNull_ThrowsArgumentNullExceptionWithCorrectParameterName()
{
// Arrange
var dependencies = _fixture.CreateTestDependencies();

// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
new TestCommandHandler(dependencies.MockEventStore.Object, dependencies.MockSerializer.Object, null!));

Assert.Equal("dateTimeService", exception.ParamName);
}

[Fact]
public void Constructor_WhenAllParametersAreValid_CreatesInstanceSuccessfully()
{
// Arrange
var dependencies = _fixture.CreateTestDependencies();

// Act
var handler = new TestCommandHandler(
dependencies.MockEventStore.Object,
dependencies.MockSerializer.Object,
dependencies.MockDateTimeService.Object);

// Assert
Assert.NotNull(handler);
Assert.IsType<TestCommandHandler>(handler);
}

[Fact]
public void Constructor_WhenAllParametersAreValid_AssignsDependenciesCorrectly()
{
// Arrange
var dependencies = _fixture.CreateTestDependencies();

// Act
var handler = new TestCommandHandler(
dependencies.MockEventStore.Object,
dependencies.MockSerializer.Object,
dependencies.MockDateTimeService.Object);

// Assert
// Verify that the handler can be used (implicitly tests that dependencies were assigned)
Assert.NotNull(handler);

// Verify the handler implements the expected interface
Assert.IsType<ICommandHandler<TestCommand, TestEvent>>(handler, exactMatch: false);
}

[Theory]
[InlineData(0)] // eventStore
[InlineData(1)] // serializer
[InlineData(2)] // dateTimeService
public void Constructor_WhenSingleParameterIsNull_ThrowsArgumentNullException(int nullParameterIndex)
{
// Arrange
var dependencies = _fixture.CreateTestDependencies();
var eventStore = nullParameterIndex == 0 ? null : dependencies.MockEventStore.Object;
var serializer = nullParameterIndex == 1 ? null : dependencies.MockSerializer.Object;
var dateTimeService = nullParameterIndex == 2 ? null : dependencies.MockDateTimeService.Object;

// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new TestCommandHandler(eventStore!, serializer!, dateTimeService!));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Threading.Tasks;
using Xunit;

namespace MathMax.EventSourcing.UnitTests.Commands;

/// <summary>
/// Tests for CommandHandlerBase edge cases and special scenarios
/// </summary>
public class CommandHandlerBaseEdgeCaseTests : IClassFixture<CommandHandlerBaseTestFixture>
{
private readonly CommandHandlerBaseTestFixture _fixture;

public CommandHandlerBaseEdgeCaseTests(CommandHandlerBaseTestFixture fixture)
{
_fixture = fixture;
}

[Fact]
public async Task HandleAsync_WithNullAggregateId_CreatesEventEnvelopeWithNullAggregateId()
{
// Arrange
var dependencies = _fixture.CreateTestDependencies();
var command = CommandHandlerBaseTestFixture.CreateTestCommand();
var handlerWithNullAggregateId = dependencies.CreateHandlerWithNullAggregateId();
var serializedEnvelope = _fixture.CreateSerializedEnvelope(null, 1);
dependencies.SetupSerializerMock(serializedEnvelope);

// Act
var result = await handlerWithNullAggregateId.HandleAsync(command);

// Assert
Assert.Null(result.AggregateId);
}

[Fact]
public async Task HandleAsync_WithNullVersion_CreatesEventEnvelopeWithNullVersion()
{
// Arrange
var dependencies = _fixture.CreateTestDependencies();
var command = CommandHandlerBaseTestFixture.CreateTestCommand();
var handlerWithNullVersion = dependencies.CreateHandlerWithNullVersion();
var serializedEnvelope = _fixture.CreateSerializedEnvelope(command.Id, null);
dependencies.SetupSerializerMock(serializedEnvelope);

// Act
var result = await handlerWithNullVersion.HandleAsync(command);

// Assert
Assert.Null(result.Version);
}

[Fact]
public void GetEventType_ReturnsCorrectEventTypeName()
{
// Arrange
var dependencies = _fixture.CreateTestDependencies();

// Act
var eventType = dependencies.Handler.PublicGetEventType();

// Assert
Assert.Equal("TestEvent", eventType);
}
}
Loading
Loading