200 lines
7.6 KiB
C#
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);
|
|
}
|