using OpenAC.MosswartMassacre.Quests; namespace OpenAC.MosswartMassacre.Tests.Quests; public sealed class QuestTests { // Real answer lines in the shape the server writes them (ACE's /myquests: // "stamp - N solves (last)\"message\" max delta"). The first is the example // in the original's parser comment, the second the one UtilityBelt quotes. private const string CommentSample = "SomeQuest - 5 solves (1640995200)\"Quest description here\" 10 3600"; private const string UbSample = "regaliamaskuber - 1 solves (1522807913)\"Player has collected the mask from a Virinidi Profatrix.\" -1 72000"; private static long UnixNow(PluginHarness harness) => new DateTimeOffset(harness.Clock.UtcNow).ToUnixTimeSeconds(); private static string Line(string stamp, int solves, long lastSolved, int max, long delta) => $"{stamp} - {solves} solves ({lastSolved})\"A message\" {max} {delta}"; [Fact] public void Login_asks_the_server_and_reads_the_answer_for_three_seconds() { using var harness = new PluginHarness(); harness.Login(); QuestManager quests = harness.Feature(); Assert.Equal(["/myquests"], harness.Host.Automation.Chat.Submitted); Assert.True(quests.IsRefreshing); harness.Host.Automation.Chat.Deliver(CommentSample); harness.Host.Automation.Chat.Deliver(UbSample); harness.Host.Automation.Chat.Deliver("You say, \"hello - 3 solves\""); harness.Run(3.1); harness.Host.Automation.Chat.Deliver("late - 1 solves (1)\"x\" 1 1"); Assert.False(quests.IsRefreshing); Assert.Equal(2, quests.QuestList.Count); Quest first = quests.QuestList[0]; Assert.Equal("SomeQuest", first.Id); Assert.Equal(5, first.Solves); Assert.Equal(1640995200, first.Timestamp); Assert.Equal("Quest description here", first.Description); Assert.Equal(10, first.MaxSolves); Assert.Equal(3600, first.Delta); Assert.Equal(1640995200 + 3600, first.ExpireTime); Quest second = quests.QuestList[1]; Assert.Equal("regaliamaskuber", second.Id); Assert.Equal(-1, second.MaxSolves); Assert.Equal(72000, second.Delta); Assert.Equal("Player has collected the mask from a Virinidi Profatrix.", second.Description); } [Fact] public void A_refresh_while_one_is_reading_is_ignored_and_the_answer_stays_in_chat() { using var harness = new PluginHarness(); harness.Login(); harness.Context.Commands.Dispatch("refreshquests"); Assert.Equal(["/myquests"], harness.Host.Automation.Chat.Submitted); Assert.Contains("[Mosswart Massacre] [QUEST] Refreshing quest data...", harness.ChatLines); harness.Host.Automation.Chat.Deliver(UbSample); // The original never ate the answer lines. Assert.Contains(harness.Host.Automation.Chat.Delivered, message => message.Text == UbSample); harness.Run(3.1); harness.Context.Commands.Dispatch("refreshquests"); Assert.Equal(["/myquests", "/myquests"], harness.Host.Automation.Chat.Submitted); Assert.Empty(harness.Feature().QuestList); } [Fact] public void A_line_whose_number_overflows_is_reported_like_the_original() { using var harness = new PluginHarness(); harness.Login(); harness.Host.Automation.Chat.Deliver("big - 1 solves (1)\"x\" 1 99999999999"); Assert.Empty(harness.Feature().QuestList); Assert.Contains(harness.ChatLines, line => line.StartsWith( "[Mosswart Massacre] Error parsing quest line 'big - 1 solves (1)\"x\" 1 99999999999': ", StringComparison.Ordinal)); } [Fact] public void The_quest_frame_is_exactly_the_original_one() { // The original: WebSocket.SendQuestDataAsync serialized // new { type = "quest", timestamp = DateTime.UtcNow.ToString("o"), // character_name = CharacterFilter.Name, quest_name, countdown } // with JsonConvert.SerializeObject, and QuestStreamingService passed // QuestManager.GetFriendlyQuestName(quest.Id) and FormatCountdown. using var harness = new PluginHarness(); harness.Login(); long now = UnixNow(harness); harness.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 3, now - 100, -1, 100 + 93600)); harness.Feature().StreamPriorityQuests(); Assert.Equal( ["{\"type\":\"quest\",\"timestamp\":\"2026-09-25T08:30:15.1230000Z\",\"character_name\":\"Mossy\",\"quest_name\":\"Stipend Collection Timer\",\"countdown\":\"1d 02h\"}"], harness.Frames); } [Fact] public void Only_the_three_priority_quests_stream_once_each_in_answer_order() { using var harness = new PluginHarness(); harness.Login(); long now = UnixNow(harness); harness.Host.Automation.Chat.Deliver(Line("insatiableeaterjaw", 1, now - 10, -1, 5)); harness.Host.Automation.Chat.Deliver(Line("somethingelse", 1, now, -1, 500)); harness.Host.Automation.Chat.Deliver(Line("augmentationblankgemacquired", 1, now, -1, 3 * 3600 + 5 * 60 + 30)); harness.Host.Automation.Chat.Deliver(Line("insatiableeaterjaw", 9, now, -1, 99999)); harness.Feature().StreamPriorityQuests(); // "insatiableeaterjaw" is in the name table twice; the later entry, // "Insatiable Eater Jaw", is the one the original sent. Assert.Equal( [ "{\"type\":\"quest\",\"timestamp\":\"2026-09-25T08:30:15.1230000Z\",\"character_name\":\"Mossy\",\"quest_name\":\"Insatiable Eater Jaw\",\"countdown\":\"READY\"}", "{\"type\":\"quest\",\"timestamp\":\"2026-09-25T08:30:15.1230000Z\",\"character_name\":\"Mossy\",\"quest_name\":\"Blank Augmentation Gem Pickup Timer\",\"countdown\":\"3h 05m\"}", ], harness.Frames); } [Fact] public void The_stream_runs_every_thirty_seconds_after_login() { using var harness = new PluginHarness(); harness.Login(); long now = UnixNow(harness); harness.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 1, now, -1, 3600 + 30)); harness.Run(29.5); Assert.Empty(harness.FramesOfType("quest")); harness.Run(1); Assert.Single(harness.FramesOfType("quest")); Assert.Equal("1h 00m", (string?)harness.FramesOfType("quest").Single()["countdown"]); harness.Run(30); Assert.Equal(2, harness.FramesOfType("quest").Count()); } [Fact] public void Nothing_streams_while_streaming_is_off_or_after_logoff() { using var off = new PluginHarness(webSocketEnabled: false); off.Login(); off.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 1, UnixNow(off), -1, 60)); off.Feature().StreamPriorityQuests(); Assert.Empty(off.Frames); using var harness = new PluginHarness(); harness.Login(); harness.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 1, UnixNow(harness), -1, 60)); harness.Plugin.OnLogoff(); harness.Run(31); Assert.Empty(harness.Frames); Assert.Equal(0, harness.Host.Automation.Chat.ReceivedSubscriberCount); } [Fact] public void Quest_status_lists_the_priority_quests() { using var harness = new PluginHarness(); harness.Login(); harness.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 1, UnixNow(harness), -1, 60)); harness.Context.Commands.Dispatch("queststatus"); Assert.Contains("[Mosswart Massacre] Timer Active: True", harness.ChatLines); Assert.Contains("[Mosswart Massacre] Quest Count: 1", harness.ChatLines); Assert.Contains("[Mosswart Massacre] Priority Quests Found: 1", harness.ChatLines); Assert.Contains("[Mosswart Massacre] - Stipend Collection Timer (stipendtimer_0812)", harness.ChatLines); } [Theory] [InlineData(-5L, "READY")] [InlineData(0L, "READY")] [InlineData(1L, "1s")] [InlineData(59L, "59s")] [InlineData(60L, "1m 00s")] [InlineData(69L, "1m 09s")] [InlineData(3599L, "59m 59s")] [InlineData(3600L, "1h 00m")] [InlineData(3660L, "1h 01m")] [InlineData(86399L, "23h 59m")] [InlineData(86400L, "1d 00h")] [InlineData(93600L, "1d 02h")] [InlineData(10L * 86400 + 23 * 3600 + 3599, "10d 23h")] public void Countdown_formats_like_the_original(long seconds, string expected) { Assert.Equal(expected, QuestStreamingService.FormatCountdown(seconds)); } [Theory] [InlineData(0, "0s")] [InlineData(-1, "0s")] [InlineData(59, "59s")] [InlineData(60, "1m")] [InlineData(3600, "1h")] [InlineData(72000, "20h")] [InlineData(90061, "1d 1h 1m 1s")] [InlineData(1209600, "14d")] public void Cooldown_formats_like_the_original(int seconds, string expected) { Assert.Equal(expected, QuestManager.FormatSeconds(seconds)); } [Fact] public void Last_solved_is_month_first_in_utc_whatever_the_culture() { System.Globalization.CultureInfo saved = System.Globalization.CultureInfo.CurrentCulture; try { System.Globalization.CultureInfo.CurrentCulture = new System.Globalization.CultureInfo("sv-SE"); Assert.Equal("04/04/2018 05:31:53", QuestManager.FormatTimeStamp(1522819913)); } finally { System.Globalization.CultureInfo.CurrentCulture = saved; } } }