diff --git a/ECMJobRunner.Tests/DTOs/CfgProfileDto.cs b/ECMJobRunner.Tests/DTOs/CfgProfileDto.cs new file mode 100644 index 0000000..b2e2e82 --- /dev/null +++ b/ECMJobRunner.Tests/DTOs/CfgProfileDto.cs @@ -0,0 +1,20 @@ +using System; + +namespace ECMJobRunner.Tests.DTOs +{ + /// + /// DTO for CfgProfile entity used in tests + /// + public class CfgProfileDto + { + public bool Active { get; set; } + public string ProfileName { get; set; } = string.Empty; + public byte TypeId { get; set; } + public string? Schedule { get; set; } + public string? Comment { get; set; } + public string AddedWho { get; set; } = string.Empty; + public DateTime AddedWhen { get; set; } + public string ChangedWho { get; set; } = string.Empty; + public DateTime ChangedWhen { get; set; } + } +} diff --git a/ECMJobRunner.Tests/ECMJobRunner.Tests.csproj b/ECMJobRunner.Tests/ECMJobRunner.Tests.csproj new file mode 100644 index 0000000..0c7d24e --- /dev/null +++ b/ECMJobRunner.Tests/ECMJobRunner.Tests.csproj @@ -0,0 +1,71 @@ + + + + net480;net8.0 + latest + enable + enable + + false + true + + + $(NoWarn);NU1903 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ECMJobRunner.Tests/Infrastructure/FakeDataGenerator.cs b/ECMJobRunner.Tests/Infrastructure/FakeDataGenerator.cs new file mode 100644 index 0000000..de7f3b9 --- /dev/null +++ b/ECMJobRunner.Tests/Infrastructure/FakeDataGenerator.cs @@ -0,0 +1,79 @@ +using Bogus; +using ECMJobRunner.Domain.Entities; + +namespace ECMJobRunner.Tests.Infrastructure; + +/// +/// Fake data generator using Bogus library +/// +public static class FakeDataGenerator +{ + private static int _profileIdCounter = 1; + private static int _sqlJobIdCounter = 1; + private static int _historyIdCounter = 1; + + /// + /// Reset ID counters (call at the beginning of each test) + /// + public static void ResetIdCounters() + { + _profileIdCounter = 1; + _sqlJobIdCounter = 1; + _historyIdCounter = 1; + } + + /// + /// Generate fake CfgProfile + /// + public static Faker CfgProfileFaker() + { + return new Faker() + .RuleFor(p => p.Id, f => _profileIdCounter++) + .RuleFor(p => p.Active, f => f.Random.Bool()) + .RuleFor(p => p.ProfileName, f => f.Commerce.ProductName()) + .RuleFor(p => p.TypeId, f => f.Random.Byte(0, 3)) + .RuleFor(p => p.Schedule, f => $"{f.Random.Int(0, 59)} {f.Random.Int(0, 23)} * * *") + .RuleFor(p => p.Comment, f => f.Lorem.Sentence()) + .RuleFor(p => p.AddedWho, f => f.Internet.UserName()) + .RuleFor(p => p.AddedWhen, f => f.Date.Past()) + .RuleFor(p => p.ChangedWho, f => f.Internet.UserName()) + .RuleFor(p => p.ChangedWhen, f => f.Date.Recent()); + } + + /// + /// Generate fake ProfileSqlJob + /// + public static Faker ProfileSqlJobFaker(long? profileId = null) + { + return new Faker() + .RuleFor(j => j.Id, f => _sqlJobIdCounter++) + .RuleFor(j => j.ProfileId, f => profileId ?? f.Random.Long(1, 100)) + .RuleFor(j => j.Active, f => f.Random.Bool()) + .RuleFor(j => j.Sequence, f => f.Random.Short(1, 100)) + .RuleFor(j => j.Name, f => f.Hacker.Verb()) + .RuleFor(j => j.SqlCheckQuery, f => $"SELECT COUNT(*) FROM {f.Database.Type()}") + .RuleFor(j => j.SqlMainQuery, f => $"SELECT * FROM {f.Database.Type()}") + .RuleFor(j => j.ApiCommand, f => f.Internet.Url()) + .RuleFor(j => j.Comment, f => f.Lorem.Sentence()) + .RuleFor(j => j.AddedWho, f => f.Internet.UserName()) + .RuleFor(j => j.AddedWhen, f => f.Date.Past()) + .RuleFor(j => j.ChangedWho, f => f.Internet.UserName()) + .RuleFor(j => j.ChangedWhen, f => f.Date.Recent()); + } + + /// + /// Generate fake ProfileHistory + /// + public static Faker ProfileHistoryFaker(long? profileId = null) + { + return new Faker() + .RuleFor(h => h.Id, f => _historyIdCounter++) + .RuleFor(h => h.ProfileId, f => profileId ?? f.Random.Long(1, 100)) + .RuleFor(h => h.ResultId, f => f.Random.Byte(0, 2)) + .RuleFor(h => h.ResultText, f => f.Lorem.Paragraph()) + .RuleFor(h => h.AddedWho, f => f.Internet.UserName()) + .RuleFor(h => h.AddedWhen, f => f.Date.Past()) + .RuleFor(h => h.ChangedWho, f => f.Internet.UserName()) + .RuleFor(h => h.ChangedWhen, f => f.Date.Recent()); + } +} diff --git a/ECMJobRunner.Tests/Infrastructure/TestFixture.cs b/ECMJobRunner.Tests/Infrastructure/TestFixture.cs new file mode 100644 index 0000000..f324885 --- /dev/null +++ b/ECMJobRunner.Tests/Infrastructure/TestFixture.cs @@ -0,0 +1,81 @@ +using AutoMapper; +using ECMJobRunner.Domain.Interfaces; +using ECMJobRunner.Infrastructure; +using ECMJobRunner.Infrastructure.Data; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace ECMJobRunner.Tests.Infrastructure; + +/// +/// Base test fixture with Dependency Injection and In-Memory Database +/// Supports both .NET Framework 4.8 (EF6 with Effort) and .NET 8 (EF Core InMemory) +/// +public class TestFixture : IDisposable +{ + private readonly IHost _host; + private IServiceScope? _scope; + + public IServiceProvider Services => _scope?.ServiceProvider ?? _host.Services; + public JobRunnerDbContext DbContext => Services.GetRequiredService(); + public IMapper Mapper => Services.GetRequiredService(); + public ICfgProfileRepository ProfileRepository => Services.GetRequiredService(); + public IProfileSqlJobRepository SqlJobRepository => Services.GetRequiredService(); + public IProfileHistoryRepository HistoryRepository => Services.GetRequiredService(); + + public TestFixture() + { + _host = Host.CreateDefaultBuilder() + .ConfigureServices((context, services) => + { + // Use AddInfrastructureInMemory extension for both frameworks + services.AddInfrastructureInMemory(); + + // Register test-specific AutoMapper profiles + services.AddAutoMapper(typeof(TestFixture).Assembly); + }) + .Build(); + + // Create a scope for scoped services + _scope = _host.Services.CreateScope(); + +#if !NET48 + // Ensure database is created (EF Core only) + DbContext.Database.EnsureCreated(); +#endif + } + + /// + /// Create a new service scope (useful for testing scoped lifetime) + /// + public IServiceScope CreateScope() + { + return _host.Services.CreateScope(); + } + + /// + /// Reset the current scope (creates a new DbContext) + /// + public void ResetScope() + { + _scope?.Dispose(); + _scope = _host.Services.CreateScope(); + } + + /// + /// Clear all data from database + /// + public async Task ClearDatabaseAsync() + { + DbContext.ProfileHistories.RemoveRange(DbContext.ProfileHistories); + DbContext.ProfileSqlJobs.RemoveRange(DbContext.ProfileSqlJobs); + DbContext.CfgProfiles.RemoveRange(DbContext.CfgProfiles); + await DbContext.SaveChangesAsync(); + } + + public void Dispose() + { + _scope?.Dispose(); + _host?.Dispose(); + } +} diff --git a/ECMJobRunner.Tests/Mapping/TestMappingProfile.cs b/ECMJobRunner.Tests/Mapping/TestMappingProfile.cs new file mode 100644 index 0000000..ccb952f --- /dev/null +++ b/ECMJobRunner.Tests/Mapping/TestMappingProfile.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using ECMJobRunner.Domain.Entities; +using ECMJobRunner.Tests.DTOs; + +namespace ECMJobRunner.Tests.Mapping +{ + /// + /// AutoMapper profile for test DTOs + /// + public class TestMappingProfile : Profile + { + /// + /// Constructor configuring test DTO mappings + /// + public TestMappingProfile() + { + // CfgProfileDto -> CfgProfile + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.SqlJobs, opt => opt.Ignore()) + .ForMember(dest => dest.ProfileHistories, opt => opt.Ignore()); + } + } +} diff --git a/ECMJobRunner.Tests/Repositories/CfgProfileRepositoryTests.cs b/ECMJobRunner.Tests/Repositories/CfgProfileRepositoryTests.cs new file mode 100644 index 0000000..18592b7 --- /dev/null +++ b/ECMJobRunner.Tests/Repositories/CfgProfileRepositoryTests.cs @@ -0,0 +1,153 @@ +using ECMJobRunner.Domain.Entities; +using ECMJobRunner.Tests.DTOs; +using ECMJobRunner.Tests.Infrastructure; +using FluentAssertions; + +namespace ECMJobRunner.Tests.Repositories; + +/// +/// Tests for CfgProfileRepository +/// +public class CfgProfileRepositoryTests : IClassFixture +{ + private readonly TestFixture _fixture; + + public CfgProfileRepositoryTests(TestFixture fixture) + { + _fixture = fixture; + FakeDataGenerator.ResetIdCounters(); + } + + [Fact] + public async Task GetByIdAsync_ExistingProfile_ReturnsProfile() + { + // Arrange + await _fixture.ClearDatabaseAsync(); + var dto = new CfgProfileDto + { + Active = true, + ProfileName = "Test Profile for GetById", + TypeId = (byte)1, + Schedule = "0 0 * * *", + Comment = "Test", + AddedWho = "TestUser", + AddedWhen = DateTime.Now, + ChangedWho = "TestUser", + ChangedWhen = DateTime.Now + }; + var addedProfile = await _fixture.ProfileRepository.AddAsync(dto); + + // Act + var result = await _fixture.ProfileRepository.GetByIdAsync(addedProfile.Id); + + // Assert + result.Should().NotBeNull(); + result!.Id.Should().Be(addedProfile.Id); + result.ProfileName.Should().Be("Test Profile for GetById"); + } + + [Fact] + public async Task GetAllAsync_ReturnsAllProfiles() + { + // Arrange + await _fixture.ClearDatabaseAsync(); + + for (int i = 0; i < 5; i++) + { + var dto = new CfgProfileDto + { + Active = true, + ProfileName = $"Profile {i}", + TypeId = (byte)1, + Schedule = "0 0 * * *", + AddedWho = "TestUser", + AddedWhen = DateTime.Now, + ChangedWho = "TestUser", + ChangedWhen = DateTime.Now + }; + await _fixture.ProfileRepository.AddAsync(dto); + } + + // Act + var result = await _fixture.ProfileRepository.GetAllAsync(); + + // Assert + result.Should().HaveCount(5); + } + + [Fact] + public async Task AddAsync_NewProfile_AddsToDatabase() + { + // Arrange + await _fixture.ClearDatabaseAsync(); + var dto = new CfgProfileDto + { + Active = true, + ProfileName = "Test Profile", + TypeId = (byte)1, + Schedule = "0 0 * * *", + Comment = "Test comment", + AddedWho = "TestUser", + AddedWhen = DateTime.Now, + ChangedWho = "TestUser", + ChangedWhen = DateTime.Now + }; + + // Act + var addedProfile = await _fixture.ProfileRepository.AddAsync(dto); + + // Assert + var result = await _fixture.ProfileRepository.GetByIdAsync(addedProfile.Id); + result.Should().NotBeNull(); + result!.ProfileName.Should().Be("Test Profile"); + } + + [Fact] + public async Task FindAsync_WithPredicate_ReturnsMatchingProfiles() + { + // Arrange + await _fixture.ClearDatabaseAsync(); + + // Add active profiles + for (int i = 0; i < 3; i++) + { + var dto = new CfgProfileDto + { + Active = true, + ProfileName = $"Active Profile {i}", + TypeId = (byte)1, + Schedule = "0 0 * * *", + AddedWho = "TestUser", + AddedWhen = DateTime.Now, + ChangedWho = "TestUser", + ChangedWhen = DateTime.Now + }; + await _fixture.ProfileRepository.AddAsync(dto); + } + + // Add inactive profiles + for (int i = 0; i < 2; i++) + { + var dto = new CfgProfileDto + { + Active = false, + ProfileName = $"Inactive Profile {i}", + TypeId = (byte)1, + Schedule = "0 0 * * *", + AddedWho = "TestUser", + AddedWhen = DateTime.Now, + ChangedWho = "TestUser", + ChangedWhen = DateTime.Now + }; + await _fixture.ProfileRepository.AddAsync(dto); + } + + // Act + var result = await _fixture.ProfileRepository.FindAsync(p => p.Active); + + // Assert + result.Should().HaveCount(3); + result.Should().OnlyContain(p => p.Active); + } +} +