using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
///
/// Gitea #89: reads only a template's
/// inheritance chain — fully included like ,
/// tracked or not on request — instead of the whole library. Asserted against real SQL on SQLite
/// with the reader commands recorded.
///
public class TemplateChainRepositoryTests : IDisposable
{
private const int UnrelatedTemplates = 40;
private const string Body = "/* script body the resolver projects */";
private readonly SqlReaderCommandRecorder _recorder = new();
private readonly ScadaBridgeDbContext _context;
private readonly TemplateEngineRepository _repository;
/// Initializes the test class with an in-memory context and repository.
public TemplateChainRepositoryTests()
{
_context = SqliteTestHelper.CreateInMemoryContext(_recorder);
_repository = new TemplateEngineRepository(_context, new TemplateGraphWatermark());
}
/// Releases the resources held by this test class.
public void Dispose()
{
_context.Database.CloseConnection();
_context.Dispose();
}
private async Task AddAsync(string name, int? parentId = null)
{
var t = new Template(name) { ParentTemplateId = parentId };
t.Attributes.Add(new TemplateAttribute($"{name}Attr1"));
t.Attributes.Add(new TemplateAttribute($"{name}Attr2"));
t.Scripts.Add(new TemplateScript($"{name}Script", Body));
_context.Templates.Add(t);
await _context.SaveChangesAsync();
return t;
}
private sealed record Seeded(int Root, int Base, int Mid, int Leaf, int Sibling, int Module);
private async Task SeedAsync()
{
var root = await AddAsync("Root");
var @base = await AddAsync("Base", root.Id);
var mid = await AddAsync("Mid", @base.Id);
var leaf = await AddAsync("Leaf", mid.Id);
var sibling = await AddAsync("Sibling", mid.Id); // Leaf's sibling: not on Leaf's chain
var module = await AddAsync("Module");
_context.Set().Add(
new TemplateComposition("m") { TemplateId = @base.Id, ComposedTemplateId = module.Id });
await _context.SaveChangesAsync();
for (var i = 0; i < UnrelatedTemplates; i++)
await AddAsync($"Unrelated{i}");
_context.ChangeTracker.Clear();
_recorder.Reset();
return new Seeded(root.Id, @base.Id, mid.Id, leaf.Id, sibling.Id, module.Id);
}
///
/// The chain is exactly the template and its ancestors — 4 templates instead of 46 — with script
/// bodies and every child collection, one full load per level; untracked when asked.
///
/// A task that represents the asynchronous operation.
[Fact]
public async Task Chain_ReadsOnlyTemplateAndAncestors_Untracked()
{
var ids = await SeedAsync();
var whole = await _repository.GetAllTemplatesAsync();
var wholeQueries = _recorder.Commands.Count;
_context.ChangeTracker.Clear();
_recorder.Reset();
var chain = await _repository.GetTemplateChainAsync(ids.Leaf, trackChanges: false);
Assert.Equal(new[] { ids.Leaf, ids.Mid, ids.Base, ids.Root }, chain.Select(t => t.Id));
Assert.Equal(6 + UnrelatedTemplates, whole.Count);
Assert.Equal(8, chain.Sum(t => t.Attributes.Count));
Assert.All(chain.SelectMany(t => t.Scripts), s => Assert.Equal(Body, s.Code));
Assert.Single(chain.Single(t => t.Id == ids.Base).Compositions);
Assert.DoesNotContain(chain, t => t.Id == ids.Module || t.Id == ids.Sibling);
// One split-query set per level, each bounded by one template.
Assert.Equal(4 * wholeQueries, _recorder.Commands.Count);
Assert.Empty(_context.ChangeTracker.Entries());
}
/// The resync's fallback read is tracked, because the resync writes through it.
/// A task that represents the asynchronous operation.
[Fact]
public async Task Chain_Tracked_WhenAsked_UnknownIdLoadsNothing()
{
var ids = await SeedAsync();
var chain = await _repository.GetTemplateChainAsync(ids.Mid, trackChanges: true);
Assert.Equal(new[] { ids.Mid, ids.Base, ids.Root }, chain.Select(t => t.Id));
Assert.Equal(3, _context.ChangeTracker.Entries().Count());
Assert.Empty(await _repository.GetTemplateChainAsync(987654, trackChanges: true));
}
}