Files
Omika/tests/Omika.Tests/Observability/Diagnostic/LocalFileTelemetryExporterTests.cs
2026-10-23 18:46:50 +02:00

200 lines
7.6 KiB
C#

using System.Text.Json;
using Omika.Core.Observability;
using Omika.Core.Observability.Diagnostic;
namespace Omika.Tests.Observability.Diagnostic;
public class LocalFileTelemetryExporterTests : IDisposable
{
private readonly string _dir = DiagnosticTestHarness.CreateTempDirectory();
[Fact]
public async Task Export_WritesValidJsonLine()
{
var exporter = new LocalFileTelemetryExporter(new LocalDiagnosticOptions { Directory = _dir });
await exporter.ExportAsync([Event()]);
await exporter.DisposeAsync();
string file = Assert.Single(Directory.EnumerateFiles(_dir, "*.jsonl"));
string[] lines = File.ReadAllLines(file);
Assert.Single(lines);
using JsonDocument doc = JsonDocument.Parse(lines[0]);
Assert.Equal("OMK-MOB-APP-001", doc.RootElement.GetProperty("eventCode").GetString());
Assert.Equal("INFO", doc.RootElement.GetProperty("level").GetString());
Assert.Equal("P3", doc.RootElement.GetProperty("priority").GetString());
}
[Fact]
public async Task Export_PreservesFrenchAccents()
{
var exporter = new LocalFileTelemetryExporter(new LocalDiagnosticOptions { Directory = _dir });
var e = new TelemetryEvent
{
EventCode = TelemetryEventCodes.MobileAppStarted,
Component = "échec-créé-démarré-télémétrie"
};
await exporter.ExportAsync([e]);
await exporter.DisposeAsync();
string content = File.ReadAllText(Assert.Single(Directory.EnumerateFiles(_dir, "*.jsonl")));
Assert.Contains("échec", content);
Assert.Contains("créé", content);
Assert.Contains("démarré", content);
Assert.Contains("télémétrie", content);
}
[Fact]
public async Task Export_SerializesUsageAndDeviceDimensions()
{
// T17-B/T18 : le fichier local doit porter appBuild, dimensions d'écran et
// usage fonctionnel (screenCode/featureCode), alignés sur le DTO transport.
var exporter = new LocalFileTelemetryExporter(new LocalDiagnosticOptions { Directory = _dir });
var e = new TelemetryEvent
{
EventCode = TelemetryEventCodes.MobileScreenView,
Component = "Navigation",
AppBuild = "1",
ScreenWidth = 1220,
ScreenHeight = 2712,
LogicalWidth = 375,
LogicalHeight = 834,
DisplayScale = 3.25,
ScreenCode = DiagnosticScreenCodes.SiteDetail,
FeatureCode = DiagnosticFeatureCodes.ReportGenerate
};
await exporter.ExportAsync([e]);
await exporter.DisposeAsync();
using JsonDocument doc = JsonDocument.Parse(
File.ReadAllLines(Assert.Single(Directory.EnumerateFiles(_dir, "*.jsonl")))[0]);
Assert.Equal("1", doc.RootElement.GetProperty("appBuild").GetString());
Assert.Equal(1220, doc.RootElement.GetProperty("screenWidth").GetInt32());
Assert.Equal(2712, doc.RootElement.GetProperty("screenHeight").GetInt32());
Assert.Equal(DiagnosticScreenCodes.SiteDetail, doc.RootElement.GetProperty("screenCode").GetString());
Assert.Equal(DiagnosticFeatureCodes.ReportGenerate, doc.RootElement.GetProperty("featureCode").GetString());
}
[Fact]
public async Task Export_SerializesExceptionDetail()
{
// Phase 6 : le fichier local (joint à l'email support) porte le détail
// d'exception (message/type/stack trace), aligné sur le DTO transport.
var exporter = new LocalFileTelemetryExporter(new LocalDiagnosticOptions { Directory = _dir });
var e = new DiagnosticEvent
{
EventCode = TelemetryEventCodes.MobileHandledException,
Message = "Échec d'écriture",
ExceptionType = "System.IO.IOException",
StackTrace = "at Omika.App.Services.SyncEngine.Run()"
};
await exporter.ExportAsync([e]);
await exporter.DisposeAsync();
using JsonDocument doc = JsonDocument.Parse(
File.ReadAllLines(Assert.Single(Directory.EnumerateFiles(_dir, "*.jsonl")))[0]);
Assert.Equal("Échec d'écriture", doc.RootElement.GetProperty("message").GetString());
Assert.Equal("System.IO.IOException", doc.RootElement.GetProperty("exceptionType").GetString());
Assert.Equal("at Omika.App.Services.SyncEngine.Run()", doc.RootElement.GetProperty("stackTrace").GetString());
}
[Fact]
public async Task ActiveExporter_AllowsConcurrentRead_CompleteJsonLines()
{
var exporter = new LocalFileTelemetryExporter(new LocalDiagnosticOptions { Directory = _dir });
// L'exporteur reste ACTIF (fichier ouvert en écriture, FileShare.Read).
await exporter.ExportAsync([Event(), Event()]);
string file = Assert.Single(Directory.EnumerateFiles(_dir, "*.jsonl"));
// Lecture concurrente : FileShare.ReadWrite (le writer autorise la lecture).
// File.ReadAllText (FileShare.Read) échouerait ici avec « used by another process ».
using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
string content = reader.ReadToEnd();
// Le JSONL lu est exploitable : deux lignes JSON complètes, aucune demi-ligne.
string[] lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(2, lines.Length);
foreach (string line in lines)
{
using JsonDocument doc = JsonDocument.Parse(line);
Assert.Equal("OMK-MOB-APP-001", doc.RootElement.GetProperty("eventCode").GetString());
}
await exporter.DisposeAsync();
}
[Fact]
public async Task Rotation_WhenSizeExceeded()
{
var exporter = new LocalFileTelemetryExporter(new LocalDiagnosticOptions
{
Directory = _dir,
MaxFileSizeBytes = 300,
MaxFiles = 20
});
for (int i = 0; i < 20; i++)
{
await exporter.ExportAsync([Event()]);
}
await exporter.DisposeAsync();
Assert.True(Directory.EnumerateFiles(_dir, "*.jsonl").Count() >= 2);
}
[Fact]
public async Task MaxFiles_IsEnforced()
{
var exporter = new LocalFileTelemetryExporter(new LocalDiagnosticOptions
{
Directory = _dir,
MaxFileSizeBytes = 100,
MaxFiles = 3
});
for (int i = 0; i < 50; i++)
{
await exporter.ExportAsync([Event()]);
}
await exporter.DisposeAsync();
Assert.True(Directory.EnumerateFiles(_dir, "*.jsonl").Count() <= 3);
}
[Fact]
public async Task InaccessibleDirectory_DoesNotThrow()
{
// Un FICHIER existant utilisé comme dossier → Directory.CreateDirectory échoue.
string fileAsDirectory = Path.Combine(_dir, "not-a-dir");
File.WriteAllText(fileAsDirectory, "x");
var exporter = new LocalFileTelemetryExporter(new LocalDiagnosticOptions { Directory = fileAsDirectory });
await exporter.ExportAsync([Event()]);
await exporter.DisposeAsync();
}
[Fact]
public async Task EmptyDirectory_NoFileCreated()
{
var exporter = new LocalFileTelemetryExporter(new LocalDiagnosticOptions { Directory = string.Empty });
await exporter.ExportAsync([Event()]);
await exporter.DisposeAsync();
Assert.Empty(Directory.EnumerateFiles(_dir, "*.jsonl"));
}
private static OmikaEvent Event() => new TelemetryEvent { EventCode = TelemetryEventCodes.MobileAppStarted };
public void Dispose() => DiagnosticTestHarness.DeleteDirectory(_dir);
}