using System.Net; using System.Text; using System.Xml; using XFEEDToolBox.Core.News; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class GalnetNewsTests { private static string Feed(string id = "one", string date = "Sat, 22 Oct 3312 00:00:00 GMT", string title = "Galnet headline") => $""" Elite Dangerous Galnet News {id}<![CDATA[{title}]]> https://www.elitedangerous.com/news/galnet/example{date} Next paragraph.]]> """; private sealed class Handler : HttpMessageHandler { public string Xml = Feed(); public HttpStatusCode Status = HttpStatusCode.OK; public bool Offline, WaitForCancellation; public readonly TaskCompletionSource Entered = new(TaskCreationOptions.RunContinuationsAsynchronously); protected override async Task SendAsync(HttpRequestMessage request, CancellationToken token) { Check.Equal(GalnetRss.FeedUrl, request.RequestUri!.AbsoluteUri); Entered.TrySetResult(); if (WaitForCancellation) await Task.Delay(Timeout.Infinite, token); if (Offline) throw new HttpRequestException("offline"); return new(Status) { Content = new StringContent(Xml, Encoding.UTF8, "application/rss+xml") }; } } [Test] public void ParsesRealGameYearAndReadablePlainText() { var article = GalnetRss.Parse(Feed(title: "Anaconda Mk II & field trials")).Single(); Check.Equal("Anaconda Mk II & field trials", article.Title); Check.Equal("First & second.\nNext paragraph.", article.Body); Check.Equal(3312, article.PublishedAt!.Value.Year); Check.Equal("22 OCT 3312", article.GameDate); Check.That(article.SourceUri?.Host == "www.elitedangerous.com"); } [Test] public void RejectsExternalEntitiesAndNonOfficialLinks() { Check.Throws(() => GalnetRss.Parse("]>&x;")); var article = GalnetRss.Parse(Feed().Replace("https://www.elitedangerous.com/news/galnet/example", "https://invalid.example/news")).Single(); Check.Equal("", article.Link); Check.That(article.SourceUri is null); Check.That(GalnetRss.OfficialUri("https://user@www.elitedangerous.com/news") is null); } [Test] public async Task PersistsNewsAcrossRestartAndRetainsItWhenOffline() { using var folder = new TemporaryDirectory(); var path = folder.File("news.json"); using var handler = new Handler(); using var http = new HttpClient(handler); var first = await new GalnetNewsService(path, http).RefreshAsync(); Check.That(first.Updated && File.Exists(path)); handler.Offline = true; var restarted = new GalnetNewsService(path, http); var saved = await restarted.LoadCachedAsync(); Check.Equal("one", saved!.Articles.Single().Id); var failed = await restarted.RefreshAsync(); Check.That(ReferenceEquals(saved, failed.Snapshot) && !failed.Updated); Check.Equal(first.Snapshot!.RetrievedAt, failed.Snapshot!.RetrievedAt); } [Test] public async Task EmptyMalformedAndOlderResponsesCannotEraseTheSavedEdition() { using var folder = new TemporaryDirectory(); var path = folder.File("news.json"); using var handler = new Handler(); using var http = new HttpClient(handler); var service = new GalnetNewsService(path, http); var original = (await service.RefreshAsync()).Snapshot; var saved = await File.ReadAllTextAsync(path); foreach (var response in new[] { "", "not news", "bad xml", Feed("old", "21 Oct 3312 00:00:00 GMT"), new string('x', GalnetRss.MaximumBytes + 1) }) { handler.Xml = response; var result = await service.RefreshAsync(); Check.That(ReferenceEquals(original, result.Snapshot) && !result.Updated); Check.Equal(saved, await File.ReadAllTextAsync(path)); } } [Test] public async Task OnlyNewOrChangedContentReplacesTheEdition() { using var folder = new TemporaryDirectory(); using var handler = new Handler(); using var http = new HttpClient(handler); var service = new GalnetNewsService(folder.File("news.json"), http); var first = await service.RefreshAsync(); var same = await service.RefreshAsync(); Check.That(!same.Updated && ReferenceEquals(first.Snapshot, same.Snapshot)); handler.Xml = Feed("two", "23 Oct 3312 00:00:00 GMT"); var next = await service.RefreshAsync(); Check.That(next.Updated); Check.Equal("two", next.Snapshot!.Articles.Single().Id); Check.Equal("two", (await new GalnetNewsService(folder.File("news.json"), http).LoadCachedAsync())!.Articles.Single().Id); } [Test] public async Task CancellingRefreshLeavesTheSavedNewsIntact() { using var folder = new TemporaryDirectory(); var path = folder.File("news.json"); using var handler = new Handler(); using var http = new HttpClient(handler); var service = new GalnetNewsService(path, http); var original = (await service.RefreshAsync()).Snapshot; var saved = await File.ReadAllTextAsync(path); handler.WaitForCancellation = true; using var stop = new CancellationTokenSource(); var pending = service.RefreshAsync(stop.Token); stop.Cancel(); try { await pending; throw new InvalidOperationException("Expected cancellation"); } catch (OperationCanceledException) { } Check.That(ReferenceEquals(original, service.Current)); Check.Equal(saved, await File.ReadAllTextAsync(path)); } [Test] public async Task InvalidCacheCanBeReplacedByValidNews() { using var folder = new TemporaryDirectory(); var path = folder.File("news.json"); await File.WriteAllTextAsync(path, """{"articles":[null]}"""); using var handler = new Handler(); using var http = new HttpClient(handler); var result = await new GalnetNewsService(path, http).RefreshAsync(); Check.That(result.Updated && result.Snapshot!.Articles.Count == 1); } } [TestFixture, Explicit("Reads Frontier's public Galnet feed; opt in manually.")] public class GalnetNewsLiveTests { [Test] public async Task ReadsOfficialFeedAndRestoresTheDownloadedEdition() { using var folder = new TemporaryDirectory(); var path = folder.File("news.json"); var result = await new GalnetNewsService(path).RefreshAsync(); Check.That(result.Snapshot is { Articles.Count: > 0 }, result.Message); var snapshot = result.Snapshot!; var restored = await new GalnetNewsService(path).LoadCachedAsync(); Check.That(restored is not null && restored.Articles.SequenceEqual(snapshot.Articles)); var report = System.IO.Path.GetFullPath("artifacts/local-news-0416/live-summary.json"); Directory.CreateDirectory(System.IO.Path.GetDirectoryName(report)!); await File.WriteAllTextAsync(report, System.Text.Json.JsonSerializer.Serialize(new { source = GalnetRss.FeedUrl, count = snapshot.Articles.Count, latestTitle = snapshot.Articles[0].Title, gameDate = snapshot.Articles[0].GameDate, snapshot.RetrievedAt, restoredAfterRestart = true }, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })); } }