Codebasis als Dateien ins Arbeitsrepo statt als Gitlink

QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
@@ -0,0 +1,389 @@
using Centron.BusinessLogic.Telemetry;
using Centron.Data.Entities.Telemetry;
using Centron.Host.AspNetCore.Telemetry;
using Centron.Interfaces.BL;
using JetBrains.Annotations;
using NSubstitute;
namespace Centron.Tests.BL.Telemetry;
[TestSubject(typeof(TelemetryAggregator))]
public class TelemetryAggregatorTest
{
private static readonly DateTime _bucket = new(2026, 5, 2, 12, 15, 0, DateTimeKind.Utc);
private static (TelemetryAggregator Aggregator, TelemetryBL BlMock) CreateAggregator()
{
var bl = Substitute.For<TelemetryBL>((Centron.DAO.DAOSession?)null);
bl.UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>())
.Returns(Result.AsSuccess());
bl.UpsertApiCallBatch(Arg.Any<IReadOnlyCollection<ApiCallBucketIncrement>>())
.Returns(Result.AsSuccess());
// Lookup-resolution always returns one I3D per distinct name (auto-numbered, deterministic).
var toolNameCounter = 0;
bl.ResolveMcpToolNameI3Ds(Arg.Any<IReadOnlyCollection<string>>())
.Returns(call =>
{
var names = call.Arg<IReadOnlyCollection<string>>();
var dict = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var n in names.Distinct(StringComparer.Ordinal))
dict[n] = ++toolNameCounter;
return Result<Dictionary<string, int>>.AsSuccess(dict);
});
var methodNameCounter = 0;
bl.ResolveApiMethodNameI3Ds(Arg.Any<IReadOnlyCollection<string>>())
.Returns(call =>
{
var names = call.Arg<IReadOnlyCollection<string>>();
var dict = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var n in names.Distinct(StringComparer.Ordinal))
dict[n] = ++methodNameCounter;
return Result<Dictionary<string, int>>.AsSuccess(dict);
});
var hardwareIdCounter = 0;
bl.ResolveHardwareIDI3Ds(Arg.Any<IReadOnlyCollection<string>>())
.Returns(call =>
{
var names = call.Arg<IReadOnlyCollection<string>>();
var dict = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var n in names.Distinct(StringComparer.Ordinal))
dict[n] = ++hardwareIdCounter;
return Result<Dictionary<string, int>>.AsSuccess(dict);
});
bl.LoadAllHardwareIDs()
.Returns(Result<List<HardwareIDLookup>>.AsSuccess(new List<HardwareIDLookup>()));
var aggregator = new TelemetryAggregator(action => action(bl));
return (aggregator, bl);
}
#region SnapToBucket
[Theory]
[InlineData(0, 0)]
[InlineData(7, 0)]
[InlineData(14, 0)]
[InlineData(15, 15)]
[InlineData(29, 15)]
[InlineData(30, 30)]
[InlineData(44, 30)]
[InlineData(45, 45)]
[InlineData(59, 45)]
public void SnapToBucket_RoundsDownTo15MinuteBoundary(int inputMinute, int expectedMinute)
{
var input = new DateTime(2026, 5, 2, 12, inputMinute, 0, DateTimeKind.Utc);
var snapped = TelemetryAggregator.SnapToBucket(input);
Assert.Equal(expectedMinute, snapped.Minute);
Assert.Equal(12, snapped.Hour);
}
[Fact]
public void SnapToBucket_PreservesUtcKind()
{
var input = new DateTime(2026, 5, 2, 12, 17, 0, DateTimeKind.Utc);
var snapped = TelemetryAggregator.SnapToBucket(input);
Assert.Equal(DateTimeKind.Utc, snapped.Kind);
}
[Fact]
public void SnapToBucket_StripsSecondsAndMilliseconds()
{
var input = new DateTime(2026, 5, 2, 12, 14, 59, 999, DateTimeKind.Utc);
var snapped = TelemetryAggregator.SnapToBucket(input);
Assert.Equal(0, snapped.Second);
Assert.Equal(0, snapped.Millisecond);
Assert.Equal(0, snapped.Minute);
}
#endregion
#region IncrementToolUsage
[Fact]
public async Task IncrementToolUsage_MergesIdenticalKeysIntoSingleBucket()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
for (int i = 0; i < 5; i++)
aggregator.IncrementToolUsage(42, "create_ticket", McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
var item = Assert.Single(captured);
Assert.Equal(42, item.UserID);
Assert.True(item.ToolNameI3D > 0);
Assert.Equal(McpToolMode.User, item.ToolMode);
Assert.Equal(_bucket, item.BucketStartUtc);
Assert.Equal(5, item.Increment);
}
[Fact]
public async Task IncrementToolUsage_DistinctKeys_CreateSeparateBuckets()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(2, "tool", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(3, "tool", McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
Assert.Equal(3, captured.Count);
Assert.All(captured, c => Assert.Equal(1, c.Increment));
Assert.Equal(new[] { 1, 2, 3 }, captured.Select(c => c.UserID).OrderBy(x => x));
}
[Fact]
public async Task IncrementToolUsage_DifferentBucketStarts_CreateSeparateBuckets()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
var laterBucket = _bucket.AddMinutes(15);
aggregator.IncrementToolUsage(7, "tool", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(7, "tool", McpToolMode.User, laterBucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
Assert.Equal(2, captured.Count);
Assert.All(captured, c => Assert.Equal(1, c.Increment));
}
[Theory]
[InlineData(0, "tool")]
[InlineData(-1, "tool")]
[InlineData(1, "")]
public async Task IncrementToolUsage_IgnoresInvalidInput(int userId, string toolName)
{
var (aggregator, bl) = CreateAggregator();
aggregator.IncrementToolUsage(userId, toolName, McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>());
}
[Fact]
public async Task IncrementToolUsage_IgnoresNullToolName()
{
var (aggregator, bl) = CreateAggregator();
aggregator.IncrementToolUsage(1, null!, McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>());
}
[Fact]
public async Task IncrementToolUsage_IsThreadSafe_UnderConcurrentLoad()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
Parallel.For(0, 10_000, _ =>
aggregator.IncrementToolUsage(99, "tool", McpToolMode.User, _bucket));
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
var item = Assert.Single(captured);
Assert.Equal(10_000, item.Increment);
}
#endregion
#region IncrementApiCall
[Theory]
[InlineData(0, "method")]
[InlineData(-1, "method")]
[InlineData(1, "")]
public async Task IncrementApiCall_IgnoresInvalidInput(int userId, string methodName)
{
var (aggregator, bl) = CreateAggregator();
aggregator.IncrementApiCall(userId, TelemetryUserKind.User, TelemetryLicenseKind.Centron, methodName, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertApiCallBatch(Arg.Any<IReadOnlyCollection<ApiCallBucketIncrement>>());
}
[Fact]
public async Task IncrementApiCall_IgnoresNullMethodName()
{
var (aggregator, bl) = CreateAggregator();
aggregator.IncrementApiCall(1, TelemetryUserKind.User, TelemetryLicenseKind.Centron, null!, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertApiCallBatch(Arg.Any<IReadOnlyCollection<ApiCallBucketIncrement>>());
}
[Fact]
public async Task IncrementApiCall_NullLicenseKind_AggregatedSeparatelyFromKnownLicense()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<ApiCallBucketIncrement>? captured = null;
bl.UpsertApiCallBatch(Arg.Do<IReadOnlyCollection<ApiCallBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
// Both calls use the SAME UserKind so the test isolates the LicenseKind splitter
// (would still pass even if UserKind were the discriminator if we varied it).
aggregator.IncrementApiCall(1, TelemetryUserKind.User, null, "method", _bucket);
aggregator.IncrementApiCall(1, TelemetryUserKind.User, TelemetryLicenseKind.Centron, "method", _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
Assert.Equal(2, captured.Count);
Assert.Contains(captured, c => c.LicenseKind == null);
Assert.Contains(captured, c => c.LicenseKind == TelemetryLicenseKind.Centron);
}
#endregion
#region FlushAsync
[Fact]
public async Task FlushAsync_OnEmptyBuckets_DoesNotCallBl()
{
var (aggregator, bl) = CreateAggregator();
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>());
bl.DidNotReceive().UpsertApiCallBatch(Arg.Any<IReadOnlyCollection<ApiCallBucketIncrement>>());
}
[Fact]
public async Task FlushAsync_PassesAggregatedIncrementsToBl()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? tools = null;
IReadOnlyCollection<ApiCallBucketIncrement>? apis = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => tools = b))
.Returns(Result.AsSuccess());
bl.UpsertApiCallBatch(Arg.Do<IReadOnlyCollection<ApiCallBucketIncrement>>(b => apis = b))
.Returns(Result.AsSuccess());
aggregator.IncrementToolUsage(1, "a", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(2, "b", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(3, "c", McpToolMode.User, _bucket);
aggregator.IncrementApiCall(1, TelemetryUserKind.User, TelemetryLicenseKind.Centron, "m1", _bucket);
aggregator.IncrementApiCall(2, TelemetryUserKind.WebAccount, TelemetryLicenseKind.Centron, "m2", _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(tools);
Assert.NotNull(apis);
Assert.Equal(3, tools.Count);
Assert.Equal(2, apis.Count);
Assert.Contains(tools, t => t.UserID == 2);
Assert.Contains(apis, a => a.UserKind == TelemetryUserKind.WebAccount);
}
[Fact]
public async Task FlushAsync_BatchesAt200ItemsPerCall()
{
var (aggregator, bl) = CreateAggregator();
var batchSizes = new List<int>();
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => batchSizes.Add(b.Count)))
.Returns(Result.AsSuccess());
for (int i = 0; i < 450; i++)
aggregator.IncrementToolUsage(i + 1, "tool", McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.Equal(new[] { 200, 200, 50 }, batchSizes);
}
[Fact]
public async Task FlushAsync_OnBlError_RestoresSnapshotIntoBuckets()
{
var (aggregator, bl) = CreateAggregator();
var responses = new Queue<Result>();
responses.Enqueue(Result.AsError("boom"));
responses.Enqueue(Result.AsSuccess());
IReadOnlyCollection<McpToolUsageBucketIncrement>? secondCallCaptured = null;
bl.UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>())
.Returns(call =>
{
var arg = call.Arg<IReadOnlyCollection<McpToolUsageBucketIncrement>>();
var result = responses.Dequeue();
if (result.Status == ResultStatus.Success)
secondCallCaptured = arg;
return result;
});
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(secondCallCaptured);
var item = Assert.Single(secondCallCaptured);
Assert.Equal(2, item.Increment);
}
[Fact]
public async Task FlushAsync_OnBlError_RestoreCombinesWithIncrementsArrivedDuringFlush()
{
var (aggregator, bl) = CreateAggregator();
var blocker = new ManualResetEventSlim(false);
var midFlight = new ManualResetEventSlim(false);
var firstCall = true;
bl.UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>())
.Returns(_ =>
{
if (firstCall)
{
firstCall = false;
midFlight.Set();
blocker.Wait();
return Result.AsError("boom");
}
return Result.AsSuccess();
});
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
var flushTask = Task.Run(() => aggregator.FlushAsync(CancellationToken.None));
midFlight.Wait();
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
blocker.Set();
await flushTask;
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
var item = Assert.Single(captured);
Assert.Equal(2, item.Increment);
}
#endregion
}
@@ -0,0 +1,286 @@
using Centron.BusinessLogic.Telemetry;
using Centron.DAO;
using Centron.DAO.Mappings.Telemetry;
using Centron.Data.Entities.Telemetry;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.Telemetry;
[TestSubject(typeof(TelemetryBL))]
public class TelemetryBLTest
{
private static readonly DateTime _bucket = new(2026, 5, 2, 12, 15, 0, DateTimeKind.Utc);
[Fact]
public void UpsertMcpToolUsageBatch_EmptyInput_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.UpsertMcpToolUsageBatch(Array.Empty<McpToolUsageBucketIncrement>());
Assert.True(result.IsSuccess);
}
[Fact]
public void UpsertMcpToolUsageBatch_NullInput_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.UpsertMcpToolUsageBatch(null);
Assert.True(result.IsSuccess);
}
[Fact]
public void UpsertApiCallBatch_EmptyInput_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.UpsertApiCallBatch(Array.Empty<ApiCallBucketIncrement>());
Assert.True(result.IsSuccess);
}
[Fact]
public void UpsertApiCallBatch_NullInput_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.UpsertApiCallBatch(null);
Assert.True(result.IsSuccess);
}
[Fact]
public void UpsertArtificialIntelligenceToolUsageBatch_EmptyInput_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.UpsertArtificialIntelligenceToolUsageBatch(Array.Empty<ArtificialIntelligenceToolUsageBucketIncrement>());
Assert.True(result.IsSuccess);
}
[Fact]
public void UpsertArtificialIntelligenceToolUsageBatch_NullInput_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.UpsertArtificialIntelligenceToolUsageBatch(null);
Assert.True(result.IsSuccess);
}
[Fact]
public void GetCompletedPendingMcpToolUsage_ReturnsAllRowsBeforeCutoffAndUnuploaded()
{
using var session = CreateSession();
var cutoff = _bucket;
for (int i = 0; i < 5; i++)
session.Save(new McpToolUsageTelemetry { UserID = i + 1, ToolNameI3D = 100 + i, ToolMode = McpToolMode.User, BucketStartUtc = cutoff.AddMinutes(-15 * (i + 1)), Count = 1 });
session.Save(new McpToolUsageTelemetry { UserID = 99, ToolNameI3D = 99, ToolMode = McpToolMode.User, BucketStartUtc = cutoff.AddMinutes(15), Count = 1 });
session.Save(new McpToolUsageTelemetry { UserID = 100, ToolNameI3D = 100, ToolMode = McpToolMode.User, BucketStartUtc = cutoff.AddMinutes(-15), Count = 1, UploadedDate = DateTime.UtcNow });
session.Flush();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.GetCompletedPendingMcpToolUsage(cutoff);
Assert.True(result.IsSuccess);
Assert.Equal(5, result.Data.Count);
Assert.All(result.Data, r => Assert.True(r.BucketStartUtc < cutoff));
Assert.All(result.Data, r => Assert.Null(r.UploadedDate));
}
[Fact]
public void GetCompletedPendingArtificialIntelligenceToolUsage_ReturnsAllRowsBeforeCutoffAndUnuploaded()
{
using var session = CreateSession();
var cutoff = _bucket;
for (int i = 0; i < 5; i++)
session.Save(new ArtificialIntelligenceToolUsageTelemetry { UserID = i + 1, ToolNameI3D = 100 + i, HardwareIDI3D = 200 + i, BucketStartUtc = cutoff.AddMinutes(-15 * (i + 1)), Count = 1 });
session.Save(new ArtificialIntelligenceToolUsageTelemetry { UserID = 99, ToolNameI3D = 99, HardwareIDI3D = 99, BucketStartUtc = cutoff.AddMinutes(15), Count = 1 });
session.Save(new ArtificialIntelligenceToolUsageTelemetry { UserID = 100, ToolNameI3D = 100, HardwareIDI3D = 100, BucketStartUtc = cutoff.AddMinutes(-15), Count = 1, UploadedDate = DateTime.UtcNow });
session.Flush();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.GetCompletedPendingArtificialIntelligenceToolUsage(cutoff);
Assert.True(result.IsSuccess);
Assert.Equal(5, result.Data.Count);
Assert.All(result.Data, r => Assert.True(r.BucketStartUtc < cutoff));
Assert.All(result.Data, r => Assert.Null(r.UploadedDate));
}
[Fact]
public void GetCompletedPendingApiCalls_ReturnsAllRowsBeforeCutoffAndUnuploaded()
{
using var session = CreateSession();
var cutoff = _bucket;
for (int i = 0; i < 5; i++)
session.Save(new ApiCallTelemetry { UserID = i + 1, UserKind = TelemetryUserKind.User, LicenseKind = TelemetryLicenseKind.Centron, MethodNameI3D = 100 + i, BucketStartUtc = cutoff.AddMinutes(-15 * (i + 1)), Count = 1 });
session.Save(new ApiCallTelemetry { UserID = 99, UserKind = TelemetryUserKind.User, LicenseKind = TelemetryLicenseKind.Centron, MethodNameI3D = 99, BucketStartUtc = cutoff.AddMinutes(15), Count = 1 });
session.Save(new ApiCallTelemetry { UserID = 100, UserKind = TelemetryUserKind.User, LicenseKind = TelemetryLicenseKind.Centron, MethodNameI3D = 100, BucketStartUtc = cutoff.AddMinutes(-15), Count = 1, UploadedDate = DateTime.UtcNow });
session.Flush();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.GetCompletedPendingApiCalls(cutoff);
Assert.True(result.IsSuccess);
Assert.Equal(5, result.Data.Count);
Assert.All(result.Data, r => Assert.True(r.BucketStartUtc < cutoff));
Assert.All(result.Data, r => Assert.Null(r.UploadedDate));
}
[Fact]
public void LoadAllMcpToolNames_ReturnsAllRows()
{
using var session = CreateSession();
session.Save(new McpToolNameLookup { Name = "tool_a" });
session.Save(new McpToolNameLookup { Name = "tool_b" });
session.Flush();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.LoadAllMcpToolNames();
Assert.True(result.IsSuccess);
Assert.Equal(2, result.Data.Count);
Assert.Contains(result.Data, r => r.Name == "tool_a");
Assert.Contains(result.Data, r => r.Name == "tool_b");
}
[Fact]
public void LoadAllApiMethodNames_ReturnsAllRows()
{
using var session = CreateSession();
session.Save(new ApiMethodNameLookup { Name = "GetCustomer" });
session.Save(new ApiMethodNameLookup { Name = "SaveReceipt" });
session.Flush();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.LoadAllApiMethodNames();
Assert.True(result.IsSuccess);
Assert.Equal(2, result.Data.Count);
}
// MarkMcpToolUsageUploaded / MarkApiCallsUploaded "happy path" with row updates is
// not unit-tested on SQLite: the raw SQL uses "dbo." schema prefix which SQLite rejects.
// Same reason for ResolveMcp*/Api* and the Upsert*Batch methods (raw MERGE statements).
// Coverage is provided by the EndToEnd tests against SQL Server.
[Fact]
public void MarkMcpToolUsageUploaded_EmptyIds_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.MarkMcpToolUsageUploaded(Array.Empty<long>(), DateTime.UtcNow);
Assert.True(result.IsSuccess);
}
[Fact]
public void MarkApiCallsUploaded_EmptyIds_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.MarkApiCallsUploaded(Array.Empty<long>(), DateTime.UtcNow);
Assert.True(result.IsSuccess);
}
[Fact]
public void MarkArtificialIntelligenceToolUsageUploaded_EmptyIds_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.MarkArtificialIntelligenceToolUsageUploaded(Array.Empty<long>(), DateTime.UtcNow);
Assert.True(result.IsSuccess);
}
[Fact]
public void RecordArtificialIntelligenceToolUsage_InvalidInput_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
Assert.True(bl.RecordArtificialIntelligenceToolUsage(0, "tool", "hardware", DateTime.UtcNow).IsSuccess);
Assert.True(bl.RecordArtificialIntelligenceToolUsage(1, null, "hardware", DateTime.UtcNow).IsSuccess);
Assert.True(bl.RecordArtificialIntelligenceToolUsage(1, string.Empty, "hardware", DateTime.UtcNow).IsSuccess);
}
[Fact]
public void RecordArtificialIntelligenceToolUsageBatch_InvalidInput_ReturnsSuccess()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
Assert.True(bl.RecordArtificialIntelligenceToolUsageBatch(0, new[] { "tool" }, "hardware", DateTime.UtcNow).IsSuccess);
Assert.True(bl.RecordArtificialIntelligenceToolUsageBatch(1, null, "hardware", DateTime.UtcNow).IsSuccess);
Assert.True(bl.RecordArtificialIntelligenceToolUsageBatch(1, Array.Empty<string>(), "hardware", DateTime.UtcNow).IsSuccess);
Assert.True(bl.RecordArtificialIntelligenceToolUsageBatch(1, new[] { null, string.Empty }, "hardware", DateTime.UtcNow).IsSuccess);
}
[Fact]
public void ResolveMcpToolNameI3Ds_EmptyInput_ReturnsEmptyDictionary()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.ResolveMcpToolNameI3Ds(Array.Empty<string>());
Assert.True(result.IsSuccess);
Assert.Empty(result.Data);
}
[Fact]
public void ResolveApiMethodNameI3Ds_EmptyInput_ReturnsEmptyDictionary()
{
using var session = CreateSession();
var bl = new TelemetryBL(new DAOSession(session));
var result = bl.ResolveApiMethodNameI3Ds(Array.Empty<string>());
Assert.True(result.IsSuccess);
Assert.Empty(result.Data);
}
private static ISession CreateSession()
{
var fluentConfig = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory())
.Mappings(m =>
{
m.FluentMappings.Add<McpToolUsageTelemetryMaps>();
m.FluentMappings.Add<ArtificialIntelligenceToolUsageTelemetryMaps>();
m.FluentMappings.Add<ApiCallTelemetryMaps>();
m.FluentMappings.Add<McpToolNameLookupMaps>();
m.FluentMappings.Add<ApiMethodNameLookupMaps>();
m.FluentMappings.Add<HardwareIDLookupMaps>();
});
var sessionFactory = fluentConfig.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfig.BuildConfiguration())
.Execute(false, true, false, session.Connection, null);
return session;
}
}
@@ -0,0 +1,73 @@
using Centron.Data.Entities.Telemetry;
using Centron.Interfaces.Administration.Logins;
using JetBrains.Annotations;
namespace Centron.Tests.BL.Telemetry;
[TestSubject(typeof(TelemetryLicenseKindMapping))]
public class TelemetryLicenseKindMappingTest
{
[Fact]
public void FromGuid_KnownLicense_ReturnsKind()
{
var kind = TelemetryLicenseKindMapping.FromGuid(LicenseGuids.Centron);
Assert.Equal(TelemetryLicenseKind.Centron, kind);
}
[Fact]
public void FromGuidString_KnownLicense_ReturnsKind()
{
var kind = TelemetryLicenseKindMapping.FromGuidString(LicenseGuids.ServiceBoard.ToString());
Assert.Equal(TelemetryLicenseKind.ServiceBoard, kind);
}
[Fact]
public void FromGuid_UnknownGuid_ReturnsNull()
{
var kind = TelemetryLicenseKindMapping.FromGuid(Guid.NewGuid());
Assert.Null(kind);
}
[Fact]
public void FromGuidString_NullOrEmpty_ReturnsNull()
{
Assert.Null(TelemetryLicenseKindMapping.FromGuidString(null));
Assert.Null(TelemetryLicenseKindMapping.FromGuidString(""));
Assert.Null(TelemetryLicenseKindMapping.FromGuidString(" "));
}
[Fact]
public void FromGuidString_Garbage_ReturnsNull()
{
Assert.Null(TelemetryLicenseKindMapping.FromGuidString("not-a-guid"));
}
[Fact]
public void ToGuid_KnownKind_ReturnsGuid()
{
var guid = TelemetryLicenseKindMapping.ToGuid(TelemetryLicenseKind.Centron);
Assert.Equal(LicenseGuids.Centron, guid);
}
[Fact]
public void ToGuid_Unknown_ReturnsNull()
{
var guid = TelemetryLicenseKindMapping.ToGuid(TelemetryLicenseKind.Unknown);
Assert.Null(guid);
}
[Fact]
public void RoundTrip_AllKnownLicenses_AreStable()
{
// Every enum value (except Unknown) must map to a real GUID and back to itself.
foreach (TelemetryLicenseKind kind in Enum.GetValues<TelemetryLicenseKind>())
{
if (kind == TelemetryLicenseKind.Unknown) continue;
var guid = TelemetryLicenseKindMapping.ToGuid(kind);
Assert.True(guid.HasValue, $"Enum value {kind} has no matching LicenseGuids field.");
var roundTrip = TelemetryLicenseKindMapping.FromGuid(guid.Value);
Assert.Equal(kind, roundTrip);
}
}
}