Umsetzung von #564. Das WebUI zeigte bisher nur den Live-Stream seit dem Prozessstart. GET /api/logs lieferte den Verlauf im Speicher, die Oberflaeche nutzte ihn nicht. Der Desktop hat einen Log-Viewer ueber die Tagesdateien. Den gibt es jetzt auch im WebUI. - GET /api/logs/files listet die vorhandenen Tage (Logs/yyyy-MM-dd.jsonl), neueste zuerst. Die .log-Textdateien und fremde Dateien bleiben aussen vor. - GET /api/logs/file filtert nach Level, Korrelations-ID und Text, alle ohne Gross-/Kleinschreibung. Hoechstens 20000 Treffer wie im Desktop, dazu Seitenaufteilung. Unlesbare Zeilen werden uebersprungen und gezaehlt. - Der Dateiname entsteht aus dem geprueften Datum, nie aus der Eingabe. "../../etc/passwd" kommt deshalb gar nicht erst in den Pfad; die Tests halten das fest. - Gelesen wird mit FileShare.ReadWrite, weil der Logger laufend an die Tagesdatei anhaengt. Oberflaeche: "Live" und "Log-Viewer (vergangene Tage)" als Unter-Tabs. "Alles kopieren" im Live-Tab, "Seite kopieren" im Viewer. navigator.clipboard gibt es nur in sicheren Kontexten, ueber LAN/VPN laeuft das WebUI aber per http. Dafuer gibt es einen Rueckweg ueber ein unsichtbares Textfeld, sonst schluege das Kopieren dort still fehl. Logzeilen werden per textContent eingesetzt, nicht per innerHTML: Logtexte enthalten, was andere eingegeben haben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
5.2 KiB
C#
127 lines
5.2 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using PolyTrader.Web.Configuration;
|
|
using PolyTraderSharp.Services;
|
|
using Xunit;
|
|
|
|
namespace PolyTrader.Tests.WebUi
|
|
{
|
|
/// <summary>
|
|
/// Log-Viewer für vergangene Tage über das WebUI (#564).
|
|
/// </summary>
|
|
public sealed class LogViewerEndpointTests : IDisposable
|
|
{
|
|
private readonly string _dir = Path.Combine(Path.GetTempPath(), "polytrader-logs-" + Guid.NewGuid().ToString("N"));
|
|
|
|
public LogViewerEndpointTests() => Directory.CreateDirectory(_dir);
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_dir, recursive: true); } catch { }
|
|
}
|
|
|
|
private void WriteDay(string date, params (string Msg, LogLevel Level, string Cid)[] lines) =>
|
|
WriteRaw(date, lines.Select(l => LogJson.Format(new LogMessageEventArgs(l.Msg, l.Level, l.Cid))).ToArray());
|
|
|
|
private void WriteRaw(string date, params string[] lines) =>
|
|
File.WriteAllLines(Path.Combine(_dir, date + ".jsonl"), lines, new UTF8Encoding(false));
|
|
|
|
private Task<WebUiTestHost> StartAsync() =>
|
|
WebUiTestHost.StartAsync(s => s.AddSingleton(new LogDirectory(_dir)));
|
|
|
|
[Fact]
|
|
public async Task Tagesliste_zeigt_nur_Tagesdateien_neueste_zuerst()
|
|
{
|
|
WriteDay("2026-09-12", ("a", LogLevel.Info, ""));
|
|
WriteDay("2026-09-13", ("b", LogLevel.Info, ""));
|
|
File.WriteAllText(Path.Combine(_dir, "2026-09-13-Info.log"), "[12:00:00] b");
|
|
File.WriteAllText(Path.Combine(_dir, "notizen.jsonl"), "{}");
|
|
await using var web = await StartAsync();
|
|
|
|
var files = await web.GetJsonAsync("/api/logs/files");
|
|
|
|
Assert.Equal(new[] { "2026-09-13", "2026-09-12" },
|
|
files.EnumerateArray().Select(f => f.GetProperty("date").GetString()).ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Filter_nach_Level_Korrelations_ID_und_Text()
|
|
{
|
|
WriteDay("2026-09-12",
|
|
("Start", LogLevel.Info, ""),
|
|
("Signal abgelehnt", LogLevel.Warning, "sig-42"),
|
|
("VPN weg", LogLevel.Error, ""));
|
|
await using var web = await StartAsync();
|
|
|
|
var all = await web.GetJsonAsync("/api/logs/file?date=2026-09-12");
|
|
var warnings = await web.GetJsonAsync("/api/logs/file?date=2026-09-12&level=warning");
|
|
var byCid = await web.GetJsonAsync("/api/logs/file?date=2026-09-12&cid=SIG-42");
|
|
var byText = await web.GetJsonAsync("/api/logs/file?date=2026-09-12&q=vpn");
|
|
|
|
Assert.Equal(3, all.GetProperty("total").GetInt32());
|
|
Assert.Equal("Signal abgelehnt", Assert.Single(warnings.GetProperty("items").EnumerateArray()).GetProperty("msg").GetString());
|
|
Assert.Equal("sig-42", Assert.Single(byCid.GetProperty("items").EnumerateArray()).GetProperty("cid").GetString());
|
|
Assert.Equal("Error", Assert.Single(byText.GetProperty("items").EnumerateArray()).GetProperty("level").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Kaputte_Zeilen_werden_uebersprungen_und_gezaehlt()
|
|
{
|
|
WriteRaw("2026-09-12",
|
|
LogJson.Format(new LogMessageEventArgs("gut", LogLevel.Info)),
|
|
"{kaputt",
|
|
"",
|
|
LogJson.Format(new LogMessageEventArgs("auch gut", LogLevel.Info)));
|
|
await using var web = await StartAsync();
|
|
|
|
var result = await web.GetJsonAsync("/api/logs/file?date=2026-09-12");
|
|
|
|
Assert.Equal(2, result.GetProperty("total").GetInt32());
|
|
Assert.Equal(1, result.GetProperty("unreadableLines").GetInt32()); // Leerzeilen zaehlen nicht
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Seitenaufteilung()
|
|
{
|
|
WriteDay("2026-09-12", Enumerable.Range(1, 120).Select(i => ($"Zeile {i}", LogLevel.Info, "")).ToArray());
|
|
await using var web = await StartAsync();
|
|
|
|
var last = await web.GetJsonAsync("/api/logs/file?date=2026-09-12&pageSize=50&page=9");
|
|
|
|
Assert.Equal(3, last.GetProperty("totalPages").GetInt32());
|
|
Assert.Equal(3, last.GetProperty("page").GetInt32()); // auf die letzte Seite begrenzt
|
|
Assert.Equal(20, last.GetProperty("items").GetArrayLength());
|
|
Assert.Equal("Zeile 101", last.GetProperty("items")[0].GetProperty("msg").GetString());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("../../etc/passwd")]
|
|
[InlineData("2026-09-12/../../geheim")]
|
|
[InlineData("2026-02-30")]
|
|
[InlineData("")]
|
|
public async Task Ungueltiges_Datum_400(string date)
|
|
{
|
|
WriteDay("2026-09-12", ("a", LogLevel.Info, ""));
|
|
await using var web = await StartAsync();
|
|
|
|
using var res = await web.Client.GetAsync("/api/logs/file?date=" + Uri.EscapeDataString(date));
|
|
|
|
Assert.Equal(400, (int)res.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Tag_ohne_Datei_404()
|
|
{
|
|
await using var web = await StartAsync();
|
|
|
|
using var res = await web.Client.GetAsync("/api/logs/file?date=2020-01-01");
|
|
|
|
Assert.Equal(404, (int)res.StatusCode);
|
|
}
|
|
}
|
|
}
|