using System.Net;
using System.Text;
using System.Text.Json;
using XFEEDToolBox.Core.News;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class GalnetPresentationTests
{
[Test] public void OfficialMetadataRetainsDifferentPicturesAndRejectsInjectedHosts()
{
const string html = """
<meta content="https://image-service.zaonce.net/current" property="og:image">
<article class="c-galnet-latest__article" data-v-123>
<a href="/news/galnet/previous"><figure><img srcset="ignored" src="https://static.zaonce.net/previous.png?x=1&y=2"></figure></a>
</article>
<article class="c-galnet-latest__article"><a href="https://evil.example/news"><img src="https://image-service.zaonce.net/no"></a></article>
<article class="c-galnet-latest__article"><a href="/news/galnet/bad"><img src="https://www.elitedangerous.com.evil.example/image"></a></article>
<article class="c-galnet-listing__item"><a href="/news/galnet/listed"><img src="https://image-service.zaonce.net/listed"></a></article>
""";
var media = GalnetMediaCatalog.Parse(html, new("https://www.elitedangerous.com/news/galnet/current"));
Check.Equal(3, media.Count);
Check.Equal("https://image-service.zaonce.net/current", media["https://www.elitedangerous.com/news/galnet/current"]);
Check.Equal("https://static.zaonce.net/previous.png?x=1&y=2", media["https://www.elitedangerous.com/news/galnet/previous"]);
Check.That(!GalnetMediaCatalog.AllowedImage("https://user@static.zaonce.net/image"));
Check.That(!GalnetMediaCatalog.AllowedImage("file:///C:/local"));
}
[Test] public void TranslationQueriesPreserveUnicodeAndStayBelowTheByteLimit()
{
var text = string.Concat(Enumerable.Repeat("Anaconda Mk II, 飞船 🚀: a long description. ", 80));
var chunks = GalnetNewsTranslator.Chunks(text).ToArray();
Check.That(chunks.Length > 1);
Check.Equal(text, string.Concat(chunks));
Check.That(chunks.All(c => Encoding.UTF8.GetByteCount(c) <= 450));
Check.That(chunks.All(c => !char.IsLowSurrogate(c[0]) && !char.IsHighSurrogate(c[^1])));
}
private sealed class TranslationHandler : HttpMessageHandler
{
public int Requests;
public bool Offline, Quota, Wait;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
{
Requests++;
Check.Equal("api.mymemory.translated.net", request.RequestUri!.Host);
Check.That(request.RequestUri.Query.Contains("langpair=en%7Czh-CN", StringComparison.OrdinalIgnoreCase));
if (Wait) await Task.Delay(Timeout.Infinite, token);
if (Offline) throw new HttpRequestException("offline");
return new(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(new { responseStatus = Quota ? 429 : 200, responseData = new { translatedText = Quota ? "MYMEMORY WARNING: quota" : "巨蟒飞船的测试新闻。" } }), Encoding.UTF8, "application/json") };
}
}
[Test] public async Task TranslationIsPersistedAndOriginalContentChangesInvalidateItsCache()
{
using var folder = new TemporaryDirectory();
using var handler = new TranslationHandler(); using var http = new HttpClient(handler);
var path = folder.File("translated.json");
var service = new GalnetNewsTranslator(path, http);
var translated = await service.TranslateAsync("Anaconda field trials", default);
Check.Equal("巨蟒飞船的测试新闻。", translated);
Check.Equal(1, handler.Requests);
handler.Offline = true;
var restarted = new GalnetNewsTranslator(path, http);
Check.Equal(translated, await restarted.TranslateAsync("Anaconda field trials", default));
Check.Equal(1, handler.Requests);
Check.That(await restarted.CachedAsync("Anaconda field trials UPDATED", default) is null);
}
[Test] public async Task FailedTranslationsAreNotCachedOrPresentedAsChineseAndQuotaBacksOff()
{
using var folder = new TemporaryDirectory();
using var handler = new TranslationHandler { Quota = true }; using var http = new HttpClient(handler);
var service = new GalnetNewsTranslator(folder.File("translated.json"), http);
for (var i = 0; i < 2; i++)
{
try { await service.TranslateAsync("Anaconda field trials", default); throw new InvalidOperationException("Expected quota failure"); }
catch (HttpRequestException) { }
}
Check.Equal(1, handler.Requests);
Check.That(await service.CachedAsync("Anaconda field trials", default) is null);
Check.That(!File.Exists(folder.File("translated.json")));
}
[Test] public async Task CancellationDoesNotTurnTheOriginalArticleIntoACachedTranslation()
{
using var folder = new TemporaryDirectory();
using var handler = new TranslationHandler { Wait = true }; using var http = new HttpClient(handler);
var service = new GalnetNewsTranslator(folder.File("translated.json"), http);
using var stop = new CancellationTokenSource(50);
try { await service.TranslateAsync("Anaconda field trials", stop.Token); throw new InvalidOperationException("Expected cancellation"); }
catch (OperationCanceledException) { }
Check.That(await service.CachedAsync("Anaconda field trials", default) is null);
}
}
[TestFixture, Explicit("Downloads public Galnet news/images and translates one public article.")]
public class GalnetNewsCarouselLiveTests
{
[Test] public async Task OfficialPicturesAndAutomaticChineseTranslationProduceARealCarouselEdition()
{
var directory = Path.GetFullPath("artifacts/news-carousel"); Directory.CreateDirectory(directory);
var news = new GalnetNewsService(Path.Combine(directory, "live-feed.json"));
var snapshot = (await news.RefreshAsync()).Snapshot ?? throw new InvalidOperationException("Official Galnet feed unavailable");
var media = new GalnetMediaCatalog(Path.Combine(directory, "live-media.json"));
await media.RefreshAsync(snapshot, default);
var translator = new GalnetNewsTranslator(Path.Combine(directory, "live-translations.json"));
var first = snapshot.Articles[0];
var title = await translator.TranslateAsync(first.Title, default);
var teaser = await translator.TranslateAsync(GalnetSlide.Preview(first.Body), default);
var body = await translator.TranslateAsync(first.Body, default);
Check.That(title.Any(c => c is >= '\u3400' and <= '\u9fff'));
Check.That(body.Any(c => c is >= '\u3400' and <= '\u9fff'));
var slides = snapshot.Articles.Take(6).Select((a, i) => new GalnetSlide(a, media.Image(a), i == 0 ? title : a.Title,
i == 0 ? teaser : GalnetSlide.Preview(a.Body), i == 0 ? body : a.Body, i == 0, i == 0)).ToArray();
Check.That(slides.Length >= 3 && slides.All(a => GalnetMediaCatalog.AllowedImage(a.Image)), "Official images are missing");
Check.That(slides.Select(a => a.Image).Distinct().Count() > 1);
await File.WriteAllTextAsync(Path.Combine(directory, "live-slides.json"), JsonSerializer.Serialize(slides, new JsonSerializerOptions(JsonSerializerDefaults.Web)));
await File.WriteAllTextAsync(Path.Combine(directory, "live-summary.json"), JsonSerializer.Serialize(new
{
source = GalnetRss.FeedUrl, count = slides.Length, officialImages = slides.Length,
differentImages = slides.Select(s => s.Image).Distinct().Count(), translatedTitle = title,
translatedParagraphs = body.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length,
restoredChinese = await new GalnetNewsTranslator(Path.Combine(directory, "live-translations.json")).CachedAsync(first.Body, default) == body,
gameCallsInvoked = false
}, new JsonSerializerOptions { WriteIndented = true }));
}
}
using System.Net;
using System.Text;
using System.Text.Json;
using XFEEDToolBox.Core.News;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class GalnetPresentationTests
{
[Test] public void OfficialMetadataRetainsDifferentPicturesAndRejectsInjectedHosts()
{
const string html = """
<meta content="https://image-service.zaonce.net/current" property="og:image">
<article class="c-galnet-latest__article" data-v-123>
<a href="/news/galnet/previous"><figure><img srcset="ignored" src="https://static.zaonce.net/previous.png?x=1&y=2"></figure></a>
</article>
<article class="c-galnet-latest__article"><a href="https://evil.example/news"><img src="https://image-service.zaonce.net/no"></a></article>
<article class="c-galnet-latest__article"><a href="/news/galnet/bad"><img src="https://www.elitedangerous.com.evil.example/image"></a></article>
<article class="c-galnet-listing__item"><a href="/news/galnet/listed"><img src="https://image-service.zaonce.net/listed"></a></article>
""";
var media = GalnetMediaCatalog.Parse(html, new("https://www.elitedangerous.com/news/galnet/current"));
Check.Equal(3, media.Count);
Check.Equal("https://image-service.zaonce.net/current", media["https://www.elitedangerous.com/news/galnet/current"]);
Check.Equal("https://static.zaonce.net/previous.png?x=1&y=2", media["https://www.elitedangerous.com/news/galnet/previous"]);
Check.That(!GalnetMediaCatalog.AllowedImage("https://user@static.zaonce.net/image"));
Check.That(!GalnetMediaCatalog.AllowedImage("file:///C:/local"));
}
[Test] public void TranslationQueriesPreserveUnicodeAndStayBelowTheByteLimit()
{
var text = string.Concat(Enumerable.Repeat("Anaconda Mk II, 飞船 🚀: a long description. ", 80));
var chunks = GalnetNewsTranslator.Chunks(text).ToArray();
Check.That(chunks.Length > 1);
Check.Equal(text, string.Concat(chunks));
Check.That(chunks.All(c => Encoding.UTF8.GetByteCount(c) <= 450));
Check.That(chunks.All(c => !char.IsLowSurrogate(c[0]) && !char.IsHighSurrogate(c[^1])));
}
private sealed class TranslationHandler : HttpMessageHandler
{
public int Requests;
public bool Offline, Quota, Wait;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
{
Requests++;
Check.Equal("api.mymemory.translated.net", request.RequestUri!.Host);
Check.That(request.RequestUri.Query.Contains("langpair=en%7Czh-CN", StringComparison.OrdinalIgnoreCase));
if (Wait) await Task.Delay(Timeout.Infinite, token);
if (Offline) throw new HttpRequestException("offline");
return new(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(new { responseStatus = Quota ? 429 : 200, responseData = new { translatedText = Quota ? "MYMEMORY WARNING: quota" : "巨蟒飞船的测试新闻。" } }), Encoding.UTF8, "application/json") };
}
}
[Test] public async Task TranslationIsPersistedAndOriginalContentChangesInvalidateItsCache()
{
using var folder = new TemporaryDirectory();
using var handler = new TranslationHandler(); using var http = new HttpClient(handler);
var path = folder.File("translated.json");
var service = new GalnetNewsTranslator(path, http);
var translated = await service.TranslateAsync("Anaconda field trials", default);
Check.Equal("巨蟒飞船的测试新闻。", translated);
Check.Equal(1, handler.Requests);
handler.Offline = true;
var restarted = new GalnetNewsTranslator(path, http);
Check.Equal(translated, await restarted.TranslateAsync("Anaconda field trials", default));
Check.Equal(1, handler.Requests);
Check.That(await restarted.CachedAsync("Anaconda field trials UPDATED", default) is null);
}
[Test] public async Task FailedTranslationsAreNotCachedOrPresentedAsChineseAndQuotaBacksOff()
{
using var folder = new TemporaryDirectory();
using var handler = new TranslationHandler { Quota = true }; using var http = new HttpClient(handler);
var service = new GalnetNewsTranslator(folder.File("translated.json"), http);
for (var i = 0; i < 2; i++)
{
try { await service.TranslateAsync("Anaconda field trials", default); throw new InvalidOperationException("Expected quota failure"); }
catch (HttpRequestException) { }
}
Check.Equal(1, handler.Requests);
Check.That(await service.CachedAsync("Anaconda field trials", default) is null);
Check.That(!File.Exists(folder.File("translated.json")));
}
[Test] public async Task CancellationDoesNotTurnTheOriginalArticleIntoACachedTranslation()
{
using var folder = new TemporaryDirectory();
using var handler = new TranslationHandler { Wait = true }; using var http = new HttpClient(handler);
var service = new GalnetNewsTranslator(folder.File("translated.json"), http);
using var stop = new CancellationTokenSource(50);
try { await service.TranslateAsync("Anaconda field trials", stop.Token); throw new InvalidOperationException("Expected cancellation"); }
catch (OperationCanceledException) { }
Check.That(await service.CachedAsync("Anaconda field trials", default) is null);
}
}
[TestFixture, Explicit("Downloads public Galnet news/images and translates one public article.")]
public class GalnetNewsCarouselLiveTests
{
[Test] public async Task OfficialPicturesAndAutomaticChineseTranslationProduceARealCarouselEdition()
{
var directory = Path.GetFullPath("artifacts/news-carousel"); Directory.CreateDirectory(directory);
var news = new GalnetNewsService(Path.Combine(directory, "live-feed.json"));
var snapshot = (await news.RefreshAsync()).Snapshot ?? throw new InvalidOperationException("Official Galnet feed unavailable");
var media = new GalnetMediaCatalog(Path.Combine(directory, "live-media.json"));
await media.RefreshAsync(snapshot, default);
var translator = new GalnetNewsTranslator(Path.Combine(directory, "live-translations.json"));
var first = snapshot.Articles[0];
var title = await translator.TranslateAsync(first.Title, default);
var teaser = await translator.TranslateAsync(GalnetSlide.Preview(first.Body), default);
var body = await translator.TranslateAsync(first.Body, default);
Check.That(title.Any(c => c is >= '\u3400' and <= '\u9fff'));
Check.That(body.Any(c => c is >= '\u3400' and <= '\u9fff'));
var slides = snapshot.Articles.Take(6).Select((a, i) => new GalnetSlide(a, media.Image(a), i == 0 ? title : a.Title,
i == 0 ? teaser : GalnetSlide.Preview(a.Body), i == 0 ? body : a.Body, i == 0, i == 0)).ToArray();
Check.That(slides.Length >= 3 && slides.All(a => GalnetMediaCatalog.AllowedImage(a.Image)), "Official images are missing");
Check.That(slides.Select(a => a.Image).Distinct().Count() > 1);
await File.WriteAllTextAsync(Path.Combine(directory, "live-slides.json"), JsonSerializer.Serialize(slides, new JsonSerializerOptions(JsonSerializerDefaults.Web)));
await File.WriteAllTextAsync(Path.Combine(directory, "live-summary.json"), JsonSerializer.Serialize(new
{
source = GalnetRss.FeedUrl, count = slides.Length, officialImages = slides.Length,
differentImages = slides.Select(s => s.Image).Distinct().Count(), translatedTitle = title,
translatedParagraphs = body.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length,
restoredChinese = await new GalnetNewsTranslator(Path.Combine(directory, "live-translations.json")).CachedAsync(first.Body, default) == body,
gameCallsInvoked = false
}, new JsonSerializerOptions { WriteIndented = true }));
}
}