This commit is contained in:
Zeeshaun
2026-03-25 14:50:13 -05:00
parent 883009770f
commit b690921fd7
592 changed files with 46618 additions and 0 deletions
@@ -0,0 +1,25 @@
using LCU.API.Internal.DocumentIngestorLib.Extensions;
namespace LCU.API.Internal.DocumentIngestorLibTests.Extensions;
public class FilterNullsAndZerosOrDefaultTest
{
[Test]
public void FilterNullsAndZerosOrDefault()
{
var notNullList = new[] { "123", "345" };
var partialList = new[] { string.Empty, "123" };
var emptyList = Array.Empty<string>();
var listEmptyStrings = new[] { string.Empty, string.Empty };
var listZero = new[] { string.Empty, "0" };
List<string>? nullValue = null;
Assert.That(new[] { "123", "345" }, Is.EqualTo(notNullList.FilterNullsAndZerosOrDefault()));
Assert.That(new[] { "123" }, Is.EqualTo(partialList.FilterNullsAndZerosOrDefault()));
Assert.IsNull(emptyList.FilterNullsAndZerosOrDefault());
Assert.IsNull(listEmptyStrings.FilterNullsAndZerosOrDefault());
Assert.IsNull(nullValue.FilterNullsAndZerosOrDefault());
Assert.IsNull(listZero.FilterNullsAndZerosOrDefault());
}
}
@@ -0,0 +1,131 @@
using FluentAssertions;
using LCU.Api.Internal.DocumentIngestorLib.Extensions.Queryables;
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
namespace LCU.API.Internal.DocumentIngestorLibTests.Extensions.Queryables;
public class DocumentProcessingInfoNotificationStatusQueryableTest
{
[Test]
public void FilterByNotificationStatusType_NotificationStatusFiltersIsNull_ReturnsUnmodifiedQueryable()
{
var processingInfo = InfoFactory();
var filter = new Filter
{
NotificationStatusFilters = null
};
var originalProcessingInfo = processingInfo.ToArray();
var filtered = processingInfo.FilterByNotificationStatusType(filter);
filtered.Should().BeEquivalentTo(originalProcessingInfo);
}
[Test]
public void FilterByNotificationStatusType_NotificationStatusFiltersIsEmpty_ReturnsUnmodifiedQueryable()
{
var processingInfo = InfoFactory();
var filter = new Filter
{
NotificationStatusFilters = new List<NotificationStatusTypes>()
};
var originalProcessingInfo = processingInfo.ToArray();
var filtered = processingInfo.FilterByNotificationStatusType(filter);
filtered.Should().BeEquivalentTo(originalProcessingInfo);
}
[Test]
public void FilterByNotificationStatusType_ProcessingInfoMemberNotificationInfoIsNull_IsFilteredOut()
{
var processingInfo = InfoFactory();
var filter = new Filter
{
NotificationStatusFilters = processingInfo
.Where(i => i.MemberNotificationInfos != null)
.SelectMany(i => i.MemberNotificationInfos.ToList())
.Select(i => i.NotificationStatusType)
.Distinct()
.ToList()
};
var filtered = processingInfo.FilterByNotificationStatusType(filter);
filtered.Should().NotContain(i => i.MemberNotificationInfos == null);
}
[Test]
public void FilterByNotificationStatusType_ProcessingInfoMemberNotificationInfoStatusNotInFilter_IsFilteredOut()
{
var processingInfo = InfoFactory();
var notIncludedStatuses = Enum.GetValues<NotificationStatusTypes>()[..(Enum.GetValues<NotificationStatusTypes>().Length / 2)];
var filter = new Filter
{
NotificationStatusFilters = processingInfo
.Where(i => i.MemberNotificationInfos != null)
.SelectMany(i => i.MemberNotificationInfos.ToList())
.Select(i => i.NotificationStatusType)
.Distinct()
.Except(notIncludedStatuses)
.ToList()
};
var filtered = processingInfo.FilterByNotificationStatusType(filter);
filtered.Should().NotContain(i => notIncludedStatuses.Contains(i.MemberNotificationInfos.FirstOrDefault().NotificationStatusType));
}
[Test]
public void FilterByNotificationStatusType_ProcessingInfoMemberNotificationInfoStatusInFilter_IsRetained()
{
var processingInfo = InfoFactory();
var includedStatuses = Enum.GetValues<NotificationStatusTypes>()[..(Enum.GetValues<NotificationStatusTypes>().Length / 2)];
var filter = new Filter
{
NotificationStatusFilters = includedStatuses.Distinct().ToList()
};
var filtered = processingInfo.FilterByNotificationStatusType(filter);
filtered.Should().OnlyContain(i => includedStatuses.Contains(i.MemberNotificationInfos.FirstOrDefault().NotificationStatusType));
}
private static IQueryable<DocumentProcessingInfo> InfoFactory()
=> Enum.GetValues<NotificationStatusTypes>()
.Distinct()
.SelectMany(status => new[]
{
new DocumentProcessingInfo
{
MemberNotificationInfos = new List<MemberNotificationInfo>
{
new()
{
NotificationStatusType = status
}
}
},
new DocumentProcessingInfo//double each status
{
MemberNotificationInfos = new List<MemberNotificationInfo>
{
new()
{
NotificationStatusType = status
}
}
}
})
.Concat(new[]
{
new DocumentProcessingInfo
{
MemberNotificationInfos = null
}
})
.AsQueryable();
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<Configurations>Debug;Release;Test</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.12" />
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Extensions\Queryables\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.API.Internal.DocumentIngestorLib\LCU.API.Internal.DocumentIngestorLib.csproj" />
<ProjectReference Include="..\LCU.Library.Internal.DocumentIngestorModels\LCU.Library.Internal.DocumentIngestorModels.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,821 @@
using FluentAssertions;
using LCU.API.Internal.DocumentIngestorLib.Persistence;
using LCU.API.Internal.DocumentIngestorLib.Repositories;
using LCU.API.Internal.DocumentIngestorLibTests;
using LCU.Library.Internal.DocumentIngestorModels.Constants;
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using System;
namespace LCU.API.Internal.DocumentIngestorLibTests.Repositories;
public class DocumentProcessingInfoRepositoryTest : IDisposable
{
private IDocumentProcessingInfoRepository _repository;
private readonly SqliteAppDbContextFactory _contextFactory = new();
private AppDbContext _context;
[SetUp]
public void Setup()
{
_context = _contextFactory.CreateContext();
_repository = new DocumentProcessingInfoRepository(_context);
}
[Test]
public async Task FindAsync_AllCases_ReturnsAllDocumentProcessingInfoRecords()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var infos = await _repository.FindAsync();
infos.Should().BeEquivalentTo(records);
}
[Test]
public async Task FindByIdAsync_RecordExists_ReturnsRecord()
{
var matchedRecord = DocumentProcessingInfoFactory();
var records = new[]
{
DocumentProcessingInfoFactory(),
matchedRecord,
DocumentProcessingInfoFactory()
};
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var info = await _repository.FindByIdAsync(matchedRecord.DocumentProcessingInfoId);
info.Should().BeEquivalentTo(matchedRecord);
}
[Test]
public async Task FindByIdAsync_RecordDoesNotExist_ReturnsNull()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var info = await _repository.FindByIdAsync(_idSeries.Next());
Assert.That(info, Is.Null);
}
[Test]
public async Task FindListByIdAsync_RecordsMatchedById_ReturnsMatched()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var matchedRecords = records.Where((_, i) => i % 2 == 0).ToArray();
var ids = matchedRecords
.Select(r => r.DocumentProcessingInfoId)
.Concat(new[] { _idSeries.Next() })
.ToList();
var infos = await _repository.FindListByIdAsync(ids);
infos.Should().BeEquivalentTo(matchedRecords);
}
[Test]
public async Task FindByIngestorRunInfoIdAsync_RecordsMatchedByRunInfo_ReturnsMatched()
{
var runInfo = IngestorRunInfoFactory();
var matchedRecords = new[]
{
DocumentProcessingInfoFactory(runInfo),
DocumentProcessingInfoFactory(runInfo),
DocumentProcessingInfoFactory(runInfo),
};
await _context.DocumentProcessingInfos.AddRangeAsync
(
DocumentProcessingInfoFactory(IngestorRunInfoFactory()),
DocumentProcessingInfoFactory(IngestorRunInfoFactory())
);
await _context.DocumentProcessingInfos.AddRangeAsync(matchedRecords);
await _context.DocumentProcessingInfos.AddRangeAsync
(
DocumentProcessingInfoFactory(IngestorRunInfoFactory()),
DocumentProcessingInfoFactory(IngestorRunInfoFactory())
);
await _context.SaveChangesAsync();
var infos = await _repository.FindByIngestorRunInfoIdAsync(runInfo.IngestorRunInfoId);
infos.Should().BeEquivalentTo(matchedRecords);
}
[Test]
public async Task FindByProcessStatusTypeAsync_RecordsMatchingType_ReturnsMatched()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
var type = records[1].StatusType;
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var infoByType = await _repository.FindByProcessStatusTypeAsync(type, null);
infoByType.Where(documentProcessingInfo => documentProcessingInfo.StatusType.Equals(type));
}
[Test]
public async Task FindByProcessStatusTypeListAsync_RecordsMatchingType_ReturnsMatched()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
var types = records.Select(u => u.StatusType).ToList();
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var infoByType = await _repository.FindByProcessStatusTypeListAsync(types, null);
infoByType.Where(documentProcessingInfo => documentProcessingInfo.StatusType.Equals(types));
}
[Test]
public async Task FindByProcessStatusTypeAsync_RecordsMatchingType_IsUnknown_ReturnsMatched()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
records[0].EntityUnknown = true;
records[2].EntityUnknown = true;
var type = records[1].StatusType;
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var infoByType = await _repository.FindByProcessStatusTypeAsync(type, true);
infoByType.Should().BeEquivalentTo(records.Where(r => r.StatusType == type && r.EntityUnknown));
}
[Test]
public async Task FindByProcessStatusTypeListAsync_RecordsMatchingType_IsUnknown_ReturnsMatched()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
records[0].EntityUnknown = true;
records[2].EntityUnknown = true;
var types = new List<ProcessStatusTypes> { records[1].StatusType };
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var infoByType = await _repository.FindByProcessStatusTypeListAsync(types, true);
infoByType.Should().BeEquivalentTo(records.Where(r => r.StatusType.Equals(types) && r.EntityUnknown));
}
[Test]
public async Task FindCompletedFirstPassByIngestorRunInfoIdAsync_MatchedByRunInfoAndProcessedAtLeastOnce_ReturnsMatched()
{
var runInfo = IngestorRunInfoFactory();
var matchedRecords = new[]
{
DocumentProcessingInfoFactory(runInfo),
DocumentProcessingInfoFactory(runInfo),
DocumentProcessingInfoFactory(runInfo),
DocumentProcessingInfoFactory(runInfo),
DocumentProcessingInfoFactory(runInfo),
};
var matchedProcessedRecords = matchedRecords
.Where((_, i) => i % 2 == 0)
.ToArray();
foreach (var record in matchedProcessedRecords)
{
record.ProcessCount = Math.Max(1, DateTime.Now.Millisecond);
}
await _context.DocumentProcessingInfos.AddRangeAsync
(
DocumentProcessingInfoFactory(IngestorRunInfoFactory()),
DocumentProcessingInfoFactory(IngestorRunInfoFactory())
);
await _context.DocumentProcessingInfos.AddRangeAsync(matchedRecords);
await _context.DocumentProcessingInfos.AddRangeAsync
(
DocumentProcessingInfoFactory(IngestorRunInfoFactory()),
DocumentProcessingInfoFactory(IngestorRunInfoFactory())
);
await _context.SaveChangesAsync();
var runCompleted = await _repository.FindCompletedFirstPassByIngestorRunInfoIdAsync(runInfo.IngestorRunInfoId);
runCompleted.Should().BeEquivalentTo(matchedProcessedRecords);
}
[Test]
public async Task AddAsync_AllCases_TracksAndDoesntSave()
{
var info = DocumentProcessingInfoFactory();
var added = await _repository.AddAsync(info);
var tracked = _context.ChangeTracker.Entries<DocumentProcessingInfo>()
.Select(e => e.Entity);
var id = info.DocumentProcessingInfoId;
var committed = _context.Database.SqlQuery<int>
(
@$"
SELECT count(1)
FROM DocumentProcessingInfo i
WHERE i.DocumentProcessingInfoId = {id}
"
).ToArray()
.Single();
added.Should().BeEquivalentTo(info);
tracked.Should().Contain(info);
committed.Should().Be(0);
}
[Test]
public async Task Update_InfoAlreadyExists_TracksAndDoesntSave()
{
var info = DocumentProcessingInfoFactory();
await _context.DocumentProcessingInfos.AddAsync(info);
await _context.SaveChangesAsync();
info.ProcessCount++;
_repository.Update(info);
var tracked = _context.ChangeTracker.Entries<DocumentProcessingInfo>()
.Where(e => e.State == EntityState.Modified)
.Select(e => e.Entity);
var id = info.DocumentProcessingInfoId;
var count = info.ProcessCount;
var updated = _context.Database.SqlQuery<int>
(
@$"
SELECT count(1)
FROM DocumentProcessingInfo i
WHERE i.DocumentProcessingInfoId = {id}
AND i.ProcessCount = {count}
"
).ToArray()
.Single();
tracked.Should().Contain(info);
updated.Should().Be(0);
}
[Test]
public async Task Remove_InfoAlreadyExists_TracksRemovalAndDoesntSave()
{
var info = DocumentProcessingInfoFactory();
await _context.DocumentProcessingInfos.AddAsync(info);
await _context.SaveChangesAsync();
_repository.Remove(info);
var tracked = _context.ChangeTracker.Entries<DocumentProcessingInfo>()
.Where(e => e.State == EntityState.Deleted)
.Select(e => e.Entity);
var id = info.DocumentProcessingInfoId;
var stored = _context.Database.SqlQuery<int>
(
@$"
SELECT count(1)
FROM DocumentProcessingInfo i
WHERE i.DocumentProcessingInfoId = {id}
"
).ToArray()
.Single();
tracked.Should().Contain(info);
stored.Should().Be(1);
}
[Test]
public async Task FilteredListAsync_ByDocType_CountsAndFiltersByDocType()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
var matchedRecords = records.Where((_, i) => i % 2 == 0).ToArray();
var docType = matchedRecords.First().DocumentType;
foreach (var record in matchedRecords)
{
record.DocumentType = docType;
}
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var filter = new Filter
{
DocType = docType,
Page = 2,
Limit = 2
};
var filteredDocuments = await _repository.FilteredListAsync(filter);
filteredDocuments.Items.Should().BeEquivalentTo(matchedRecords.Skip((filter.Page - 1) * filter.Limit).Take(filter.Limit));
filteredDocuments.Total.Should().Be(matchedRecords.Length);
}
[Test]
public async Task FilteredListAsync_ByBusinessName_CountsAndFiltersByBusinessName()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
var matchedRecords = records.Where((_, i) => i % 2 == 0).ToArray();
var businessName = matchedRecords.First().DocumentBusinessName;
foreach (var record in matchedRecords)
{
record.DocumentBusinessName = businessName;
}
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var filter = new Filter
{
DocBusinessName = businessName,
Page = 2,
Limit = 2
};
var filteredDocuments = await _repository.FilteredListAsync(filter);
filteredDocuments.Items.Should().BeEquivalentTo(matchedRecords.Skip((filter.Page - 1) * filter.Limit).Take(filter.Limit));
filteredDocuments.Total.Should().Be(matchedRecords.Length);
}
[Test]
public async Task FilteredListAsync_ByStatusType_CountsAndFiltersByStatusType()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
var matchedRecords = records.Where((_, i) => i % 2 == 0).ToArray();
var statusType = (ProcessStatusTypes)int.MinValue;
foreach (var record in matchedRecords)
{
record.StatusType = statusType;
}
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var filter = new Filter
{
StatusFilters = records
.Select(i => i.StatusType)
.Distinct()
.Select(s => new StatusFilter { IsEnabled = s == statusType, StatusType = s })
.ToList(),
Page = 2,
Limit = 2
};
var filteredDocuments = await _repository.FilteredListAsync(filter);
filteredDocuments.Items.Should().BeEquivalentTo(matchedRecords.Skip((filter.Page - 1) * filter.Limit).Take(filter.Limit));
filteredDocuments.Total.Should().Be(matchedRecords.Length);
}
[Test]
public async Task FilteredListAsync_ByCreatedDate_CountsAndFiltersByCreatedDate()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
var matchedRecords = records.Where((_, i) => i % 2 == 0).ToArray();
var createdDate = matchedRecords.First().CreatedDate;
foreach (var record in matchedRecords)
{
record.CreatedDate = createdDate;
}
foreach (var record in records.Where(r => !matchedRecords.Contains(r)))
{
record.CreatedDate = createdDate.AddDays(1);
}
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var filter = new Filter
{
CreatedDateStart = createdDate.AddMinutes(-1),
CreatedDateEnd = createdDate.AddMinutes(1),
Page = 2,
Limit = 2
};
var filteredDocuments = await _repository.FilteredListAsync(filter);
filteredDocuments.Items.Should().HaveCount(filter.Limit);
filteredDocuments.Items.Should().BeSubsetOf(matchedRecords);
filteredDocuments.Total.Should().Be(matchedRecords.Length);
}
[Test]
public async Task FilteredListAsync_ByIngestorRunInfoId_CountsAndFiltersByCreatedDate()
{
var runInfo = IngestorRunInfoFactory();
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
var matchedRecords = records.Where((_, i) => i % 2 == 0).ToArray();
foreach (var record in matchedRecords)
{
record.IngestorRunInfo = runInfo;
}
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var filter = new Filter
{
IngestorRunInfoId = runInfo.IngestorRunInfoId,
Page = 2,
Limit = 2
};
var filteredDocuments = await _repository.FilteredListAsync(filter);
filteredDocuments.Items.Should().HaveCount(filter.Limit);
filteredDocuments.Items.Should().BeSubsetOf(matchedRecords);
filteredDocuments.Total.Should().Be(matchedRecords.Length);
}
[Test]
[Ignore("a real SQL server connection in EF is required for the json filter to work\nthe filter is tested on its own in an integration test")]
public async Task FilteredListAsync_ByIndexFileJsonKeyValue_CountsAndFiltersByDocumentValuePayloadContainingJson()
{
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
var matchedRecords = records.Where((_, i) => i % 2 == 0).ToArray();
var property = JObject.Parse(matchedRecords.First().DocumentValuePayload)
.Properties()
.First();
foreach (var record in matchedRecords)
{
var payload = JObject.Parse(record.DocumentValuePayload);
payload[property.Name] = property.Value;
record.DocumentValuePayload = payload.ToString().Replace(" ", string.Empty);
}
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var filter = new Filter
{
CustomIndexFileFiltersEquals = new Dictionary<string, string>
{
[property.Name] = property.Value.ToString()
},
Page = 2,
Limit = 2
};
var filteredDocuments = await _repository.FilteredListAsync(filter);
filteredDocuments.Items.Should().BeEquivalentTo(matchedRecords.Skip((filter.Page - 1) * filter.Limit).Take(filter.Limit));
filteredDocuments.Total.Should().Be(matchedRecords.Length);
}
[Test]
public async Task FilteredListAsync_ByNotificationStatus_CountsAndFiltersByMemberNotificationInfoStatusType()
{
var notificationStatus = NotificationStatusTypesFactory();
var records = new[]
{
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory(),
DocumentProcessingInfoFactory()
};
var matchedRecords = records.Where((_, i) => i % 2 == 0).ToArray();
foreach (var record in matchedRecords)
{
if (!record.MemberNotificationInfos.Any()) continue;
foreach (var notification in record.MemberNotificationInfos)
{
notification.NotificationStatusType = notificationStatus;
}
}
foreach (var record in records.Except(matchedRecords))
{
if (!record.MemberNotificationInfos.Any()) continue;
foreach (var notification in record.MemberNotificationInfos)
{
notification.NotificationStatusType = NotificationStatusTypesFactory(not: notificationStatus);
}
}
await _context.DocumentProcessingInfos.AddRangeAsync(records);
await _context.SaveChangesAsync();
var filter = new Filter
{
NotificationStatusFilters = new List<NotificationStatusTypes> { notificationStatus },
Page = 2,
Limit = 2
};
var filteredDocuments = await _repository.FilteredListAsync(filter);
filteredDocuments.Items.Should().BeEquivalentTo(matchedRecords.Skip((filter.Page - 1) * filter.Limit).Take(filter.Limit));
filteredDocuments.Total.Should().Be(matchedRecords.Length);
}
[Test]
public async Task CountCompletedFirstPassByIngestorRunInfoIdAsync_AllCases_ReturnsCountOfDocumentProcessingInfoWithIngestorRunInfoIdAndProcessCountMoreThanOne()
{
var ingestorRunInfo = IngestorRunInfoFactory();
var runInfos = new List<IngestorRunInfo> { ingestorRunInfo };
var documents = Enumerable.Repeat
(
() => DocumentProcessingInfoFactory(ingestorRunInfo),
100
).Select(f => f())
.ToArray();
var expectedCount = 0;
foreach (var (document, index) in documents.Select((d, i) => (d, i)))
{
document.ProcessCount = index % 2 == 0 ? 0 : 1;
if (index % 3 == 0)
{
document.IngestorRunInfoId = ingestorRunInfo.IngestorRunInfoId;
document.IngestorRunInfo = ingestorRunInfo;
}
else
{
var run = IngestorRunInfoFactory();
runInfos.Add(run);
document.IngestorRunInfoId = run.IngestorRunInfoId;
document.IngestorRunInfo = run;
}
expectedCount += index % 2 == 0 && index % 3 == 0 ? 1 : 0;
}
await _context.IngestorRunInfos.AddRangeAsync(runInfos);
await _context.DocumentProcessingInfos.AddRangeAsync(documents);
await _context.SaveChangesAsync();
var count = await _repository.CountCompletedFirstPassByIngestorRunInfoIdAsync(ingestorRunInfo.IngestorRunInfoId);
Assert.That(count, Is.EqualTo(expectedCount));
}
[Test]
public async Task FindByDocumentIdsAsync_NullDocumentId_NotReturned()
{
var info = DocumentProcessingInfoFactory();
info.DocumentId = null;
var ids = Enumerable.Repeat(Guid.NewGuid, 5)
.Select(f => f())
.ToArray();
await _context.DocumentProcessingInfos.AddRangeAsync(info);
await _context.SaveChangesAsync();
var infos = await _repository.FindByDocumentIdsAsync(ids);
CollectionAssert.DoesNotContain(infos, info);
}
[Test]
public async Task FindByDocumentIdsAsync_DocumentIdNotInIds_NotReturned()
{
var info = DocumentProcessingInfoFactory();
info.DocumentId = Guid.NewGuid();
var ids = Enumerable.Repeat(Guid.NewGuid, 5)
.Select(f => f())
.ToArray();
await _context.DocumentProcessingInfos.AddRangeAsync(info);
await _context.SaveChangesAsync();
var infos = await _repository.FindByDocumentIdsAsync(ids);
CollectionAssert.DoesNotContain(infos, info);
}
[Test]
public async Task FindByDocumentIdsAsync_DocumentIdsInIds_ReturnsAllMatched()
{
var expectedInfos = Enumerable.Repeat(DocumentProcessingInfoFactory, 5)
.Select(f =>
{
var info = f(null);
info.DocumentId = Guid.NewGuid();
return info;
})
.ToArray();
var ids = Enumerable.Repeat(Guid.NewGuid, expectedInfos.Length)
.Select(f => f())
.Concat(expectedInfos.Select(i => i.DocumentId.Value))
.Concat
(
Enumerable.Repeat(Guid.NewGuid, expectedInfos.Length)
.Select(f => f())
)
.ToArray();
await _context.DocumentProcessingInfos.AddRangeAsync(expectedInfos);
await _context.SaveChangesAsync();
var infos = await _repository.FindByDocumentIdsAsync(ids);
infos.Should().Contain(expectedInfos);
}
public void Dispose()
{
_contextFactory.Dispose();
}
private readonly Random _idSeries = new(DateTime.Now.Millisecond);
private readonly IngestorRunInfo _runInfo = IngestorRunInfoFactory();
private static IngestorRunInfo IngestorRunInfoFactory()
{
return new()
{
EndTimeStamp = DateTime.Now,
IngestorType = IngestorTypes.FosDnaDocument,
IngestorRunInfoId = Guid.NewGuid(),
IndexTotal = DateTime.Now.Millisecond,
IndexFileName = Guid.NewGuid().ToString(),
IndexFilePath = Guid.NewGuid().ToString(),
StartTimeStamp = DateTime.Now
};
}
private ProcessStatusTypes ProcessStatusTypesFactory()
=> EnumValueFactory<ProcessStatusTypes>();
private NotificationStatusTypes NotificationStatusTypesFactory(NotificationStatusTypes? not = null)
=> EnumValueFactory(not);
private T EnumValueFactory<T>(T? not = null) where T : struct, Enum
{
var availableValues = Enum.GetValues<T>().Where(t => not == null || !t.Equals(not.Value)).ToArray();
return availableValues[_idSeries.Next() % availableValues.Length];
}
private DocumentProcessingInfo DocumentProcessingInfoFactory(IngestorRunInfo? runInfo = null)
=> new()
{
CreatedDate = DateTime.Now,
DocumentFileName = Guid.NewGuid().ToString(),
DocumentId = Guid.NewGuid(),
DocumentType = Guid.NewGuid().ToString(),
DocumentBusinessName = Guid.NewGuid().ToString(),
DocumentValuePayload = @$"
{{
""{Guid.NewGuid()}"":""{Guid.NewGuid().ToString()}"",
""{Guid.NewGuid()}"":""{Guid.NewGuid().ToString()}"",
""{Guid.NewGuid()}"":""{Guid.NewGuid().ToString()}""
}}
",
DocumentProcessingInfoId = _idSeries.Next(),
IngestorRunInfoId = (runInfo ?? _runInfo).IngestorRunInfoId,
IngestorRunInfo = runInfo ?? _runInfo,
ProcessCount = 0,
StatusType = ProcessStatusTypesFactory(),
MemberNotificationInfos = NotificationInfoFactory()
};
private List<MemberNotificationInfo> NotificationInfoFactory()
=> new() {
new MemberNotificationInfo
{
NotificationStatusType = NotificationStatusTypesFactory()
},
new MemberNotificationInfo
{
NotificationStatusType = NotificationStatusTypesFactory()
},
new MemberNotificationInfo
{
NotificationStatusType = NotificationStatusTypesFactory()
}
};
}
@@ -0,0 +1,133 @@
using FluentAssertions;
using LCU.API.Internal.DocumentIngestorLib.Persistence;
using LCU.API.Internal.DocumentIngestorLib.Repositories;
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
namespace LCU.API.Internal.DocumentIngestorLibTests.Repositories;
public class MemberNotificationInfoRepositoryTest : IDisposable
{
private IMemberNotificationInfoRepository _repository;
private readonly SqliteAppDbContextFactory _contextFactory = new();
private AppDbContext _context;
[SetUp]
public void Setup()
{
_context = _contextFactory.CreateContext();
_repository = new MemberNotificationInfoRepository(_context);
}
[Test]
public async Task FindNotificationByDocumentProcessingInfoIdAsync_NoneMatchedByDocumentProcessingInfoId_ReturnsNull()
{
_context.MemberNotificationInfos.RemoveRange(_context.MemberNotificationInfos);
await _context.SaveChangesAsync();
var notifications = await _repository.FindByDocumentProcessingInfoIdAsync(DateTime.Now.Millisecond);
Assert.That(notifications, Is.Empty);
}
[Test]
public async Task FindNotificationByDocumentProcessingInfoIdAsync_OneMatchedByDocumentProcessingInfoId_ReturnsRecord()
{
var documentProcessingInfo = (await _context.DocumentProcessingInfos.AddAsync(DocumentProcessingInfoFactory())).Entity;
var notificationRecord = (await _context.MemberNotificationInfos.AddAsync(MemberNotificationInfoFactory(documentProcessingInfo.DocumentProcessingInfoId))).Entity;
await _context.SaveChangesAsync();
var notification = await _repository.FindByDocumentProcessingInfoIdAsync(notificationRecord.DocumentProcessingInfoId);
notification.FirstOrDefault().Should().BeEquivalentTo(notificationRecord);
}
[Test]
public async Task FindNotificationByDocumentProcessingInfoIdAsync_MultipleMatchedByDocumentProcessingInfoId_ReturnsFirst()
{
var documentProcessingInfo = (await _context.DocumentProcessingInfos.AddAsync(DocumentProcessingInfoFactory())).Entity;
var notificationRecord = MemberNotificationInfoFactory(documentProcessingInfo.DocumentProcessingInfoId);
await _context.MemberNotificationInfos.AddRangeAsync(notificationRecord,
MemberNotificationInfoFactory(notificationRecord.DocumentProcessingInfoId),
MemberNotificationInfoFactory(notificationRecord.DocumentProcessingInfoId));
await _context.SaveChangesAsync();
var notification = await _repository.FindByDocumentProcessingInfoIdAsync(documentProcessingInfo.DocumentProcessingInfoId);
notification.FirstOrDefault()?.DocumentProcessingInfoId.Equals(documentProcessingInfo.DocumentProcessingInfoId);
}
[Test]
public async Task GetMemberNotificationInfosByFilter_ReturnsRecords()
{
var documentProcessingInfos = new List<DocumentProcessingInfo>
{
DocumentProcessingInfoFactory(DateTime.Now.Millisecond),
DocumentProcessingInfoFactory(DateTime.Now.Millisecond + 1),
DocumentProcessingInfoFactory(DateTime.Now.Millisecond + 2)
};
await _context.DocumentProcessingInfos.AddRangeAsync(documentProcessingInfos);
await _context.MemberNotificationInfos.AddRangeAsync(new List<MemberNotificationInfo>()
{
new() {
DocumentProcessingInfoId = documentProcessingInfos[0].DocumentProcessingInfoId,
NotificationStatusType = NotificationStatusTypes.CriticalFailure
},
new() {
DocumentProcessingInfoId = documentProcessingInfos[1].DocumentProcessingInfoId,
NotificationStatusType = NotificationStatusTypes.CriticalFailure
},
new() {
DocumentProcessingInfoId = documentProcessingInfos[2].DocumentProcessingInfoId,
NotificationStatusType = NotificationStatusTypes.Complete
}
}
);
await _context.SaveChangesAsync();
var filter = new Filter
{
NotificationStatusFilters = new List<NotificationStatusTypes>
{
NotificationStatusTypes.CriticalFailure
}
};
var notificationInfos = await _repository.GetMemberNotificationInfosByFilter(filter);
Assert.That(notificationInfos.Count(), Is.EqualTo(2));
Assert.That(notificationInfos.FirstOrDefault()?.NotificationStatusType, Is.EqualTo(filter.NotificationStatusFilters.FirstOrDefault()));
}
public void Dispose()
{
_contextFactory.Dispose();
}
private static MemberNotificationInfo MemberNotificationInfoFactory(int? documentProcessingInfoId = null)
=> new()
{
DocumentProcessingInfoId = documentProcessingInfoId ?? DateTime.Now.Millisecond
};
private static DocumentProcessingInfo DocumentProcessingInfoFactory(int? documentProcessingInfoId = null)
=> new()
{
DocumentProcessingInfoId = documentProcessingInfoId ?? DateTime.Now.Millisecond,
CreatedDate = DateTime.Now,
DocumentValuePayload = "test",
StatusType = ProcessStatusTypes.Completed,
ProcessCount = 1,
DocumentType = "TaxDocument",
DocumentBusinessName = "1099-MISC",
EntityUnknown = false,
IngestorRunInfo = new IngestorRunInfo()
{
IngestorRunInfoId = Guid.NewGuid(),
IndexTotal = 1,
IndexFileName = "test"
}
};
}
@@ -0,0 +1,120 @@
using LCU.API.Internal.DocumentIngestorLib.Persistence;
using LCU.API.Internal.DocumentIngestorLib.Repositories;
using LCU.API.Internal.DocumentIngestorLibTests;
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
namespace LCU.API.Internal.DocumentIngestorLibTests.Repositories;
public class ProcessingLogRepositoryTest
{
private IProcessingLogRepository _repository;
private readonly SqliteAppDbContextFactory _contextFactory = new();
private AppDbContext _context;
[SetUp]
public void Setup()
{
_context = _contextFactory.CreateContext();
_repository = new ProcessingLogRepository(_context);
}
[Test]
public async Task FindAsync_ReturnsRecord()
{
var id = DateTime.Now.Millisecond;
await _context.DocumentProcessingInfos.AddAsync(DocumentProcessingInfoFactory(id));
var processingLogs = new List<ProcessingLog>
{ProcessingLogFactory(id + 1), ProcessingLogFactory(id + 2), ProcessingLogFactory(id + 3)};
processingLogs.ForEach(x => x.DocumentProcessingInfoId = id);
await _context.ProcessingLogs.AddRangeAsync(processingLogs);
await _context.SaveChangesAsync();
var logs = await _repository.FindAsync();
Assert.That(logs.Count(), Is.EqualTo(3));
}
[Test]
public async Task FindByIdAsync_ReturnsRecord()
{
var id = DateTime.Now.Millisecond;
await _context.DocumentProcessingInfos.AddAsync(DocumentProcessingInfoFactory(id + 1));
var log = ProcessingLogFactory(id);
log.DocumentProcessingInfoId = id + 1;
await _context.ProcessingLogs.AddAsync(log);
await _context.SaveChangesAsync();
var logs = await _repository.FindByIdAsync(id);
Assert.That(logs.ProcessingLogId, Is.EqualTo(id));
}
[Test]
public async Task FindByIdAsync_ReturnsNull()
{
var id = DateTime.Now.Millisecond;
await _context.DocumentProcessingInfos.AddAsync(DocumentProcessingInfoFactory(id + 1));
var log = ProcessingLogFactory(id);
log.DocumentProcessingInfoId = id + 1;
await _context.ProcessingLogs.AddAsync(log);
await _context.SaveChangesAsync();
var logs = await _repository.FindByIdAsync(id + 5);
Assert.That(logs, Is.Null);
}
[Test]
public async Task FindByDocumentProcessingInfoIdAsync_ReturnsRecord()
{
var id = DateTime.Now.Millisecond;
await _context.DocumentProcessingInfos.AddAsync(DocumentProcessingInfoFactory(id));
var log = ProcessingLogFactory();
log.DocumentProcessingInfoId = id;
await _context.ProcessingLogs.AddAsync(log);
await _context.SaveChangesAsync();
var logs = await _repository.FindByDocumentProcessingInfoIdAsync(id);
Assert.That(logs.FirstOrDefault()?.DocumentProcessingInfoId, Is.EqualTo(id));
}
[Test]
public async Task FindByDocumentProcessingInfoIdAsync_ReturnsNull()
{
var id = DateTime.Now.Millisecond;
await _context.DocumentProcessingInfos.AddAsync(DocumentProcessingInfoFactory(id));
var log = ProcessingLogFactory();
log.DocumentProcessingInfoId = id;
await _context.ProcessingLogs.AddAsync(log);
await _context.SaveChangesAsync();
var logs = await _repository.FindByDocumentProcessingInfoIdAsync(id + 5);
Assert.That(logs, Is.Empty);
}
private static ProcessingLog ProcessingLogFactory(int? processingLogId = null)
=> new()
{
ProcessingLogId = processingLogId ?? DateTime.Now.Millisecond,
DocumentProcessingInfoId = DateTime.Now.Millisecond,
LogMessage = Guid.NewGuid().ToString(),
};
private static DocumentProcessingInfo DocumentProcessingInfoFactory(int? documentProcessingInfoId = null)
=> new()
{
DocumentProcessingInfoId = documentProcessingInfoId ?? DateTime.Now.Millisecond,
CreatedDate = DateTime.Now,
DocumentValuePayload = "test",
StatusType = ProcessStatusTypes.Completed,
ProcessCount = 1,
DocumentType = "TaxDocument",
DocumentBusinessName = "1099-MISC",
EntityUnknown = false,
IngestorRunInfo = new IngestorRunInfo()
{
IngestorRunInfoId = Guid.NewGuid(),
IndexTotal = 1,
IndexFileName = "test"
}
};
}
@@ -0,0 +1,138 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Data;
using Docker.DotNet;
using Docker.DotNet.Models;
using LCU.API.Internal.DocumentIngestorLib.Persistence;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
namespace LCU.API.Internal.DocumentIngestorLibTests;
public class SqlServerDbContextFactory : IAsyncDisposable
{
private const int port = 1434;
private const string password = "yourStrong(!)Password";
private readonly IList<AppDbContext> _contexts = new List<AppDbContext>();
private static readonly SemaphoreSlim ContainerUse = new(1);
private static CreateContainerResponse? Container;
private static readonly IDictionary<AppDbContext, bool> _activeContexts = new ConcurrentDictionary<AppDbContext, bool>();
public async Task<AppDbContext> CreateContext()
{
await ContainerUse.WaitAsync();
Container ??= await DatabaseContainerFactory();
ContainerUse.Release();
var connectionString =
$"Data Source=localhost,{port};User Id=sa;Password={password};Database={Guid.NewGuid()};MultipleActiveResultSets=True;App=EntityFramework;Trust Server Certificate=true";
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(connectionString)
.EnableSensitiveDataLogging()
.Options;
var context = new AppDbContext(options);
await DatabaseConnectionReady(connectionString);
await context.Database.EnsureCreatedAsync();
_contexts.Add(context);
_activeContexts[context] = true;
return context;
}
private static async Task<CreateContainerResponse> DatabaseContainerFactory()
{
const string ImageName = "mcr.microsoft.com/mssql/server:2019-CU18-ubuntu-20.04";
const int containerPort = 1433;
using var docker = new DockerClientConfiguration().CreateClient();
await docker.Images.CreateImageAsync
(
new ImagesCreateParameters
{
FromImage = ImageName
},
null,
new Progress<JSONMessage>()
);
var container = await docker.Containers.CreateContainerAsync
(
new CreateContainerParameters
{
Image = ImageName,
Name = "LCU.API.Internal.DocumentIngestor.IntegrationTestDatabase",
Env = new List<string>
{
"ACCEPT_EULA=Y",
$"MSSQL_SA_PASSWORD={password}"
},
ExposedPorts = new Dictionary<string, EmptyStruct>
{
{$"{containerPort}", default}
},
HostConfig = new HostConfig
{
PortBindings = new Dictionary<string, IList<PortBinding>>
{
{ $"{containerPort}", new List<PortBinding> { new() { HostPort = $"{port}" } } }
}
}
}
);
await docker.Containers.StartContainerAsync(container.ID, new ContainerStartParameters());
return container;
}
private static async Task DatabaseConnectionReady(string connectionString)
{
await using var connection = new SqlConnection(connectionString);
var ready = false;
var triesRemaining = 20;
while (!ready && triesRemaining > 0)
{
triesRemaining--;
try
{
await connection.OpenAsync();
}
catch
{
await Task.Delay(TimeSpan.FromMilliseconds(200));
}
ready = connection.State == ConnectionState.Open;
}
}
public async ValueTask DisposeAsync()
{
foreach (var context in _contexts)
{
await context.DisposeAsync();
_activeContexts[context] = false;
}
await ContainerUse.WaitAsync();
if (_activeContexts.Values.All(a => a == false))
{
await DisposeContainer(Container);
Container = null;
}
ContainerUse.Release();
}
private static async Task DisposeContainer(CreateContainerResponse container)
{
using var docker = new DockerClientConfiguration().CreateClient();
await docker.Containers.StopContainerAsync(container.ID, new ContainerStopParameters());
await docker.Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters());
}
}
@@ -0,0 +1,35 @@
using LCU.API.Internal.DocumentIngestorLib.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace LCU.API.Internal.DocumentIngestorLibTests;
public class SqliteAppDbContextFactory : IDisposable
{
private readonly IList<SqliteConnection> _connections = new List<SqliteConnection>();
private readonly IList<AppDbContext> _contexts = new List<AppDbContext>();
public AppDbContext CreateContext()
{
var connection = new SqliteConnection("Filename=:memory:");
_connections.Add(connection);
connection.Open();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.EnableSensitiveDataLogging()
.Options;
var context = new AppDbContext(options);
context.Database.EnsureCreated();
_contexts.Add(context);
return context;
}
public void Dispose()
{
foreach (var connection in _connections) connection.Dispose();
foreach (var context in _contexts) context.Dispose();
}
}
@@ -0,0 +1 @@
global using NUnit.Framework;