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);
+ }
+}
+