Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions SubathonManager.Server/WebServer.Overlays.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,19 @@ internal async Task HandleWidgetRequest(IHttpContext ctx) {
if (Path.GetExtension(filePath).Equals(".html", StringComparison.OrdinalIgnoreCase)) {
string html = WidgetFiles.Current.ReadAllText(filePath) ?? string.Empty;

string clientScript = GetWebsocketInjectionScript();

var cssOverrides = new StringBuilder();
cssOverrides.AppendLine(GetWebsocketInjectionScript());
cssOverrides.AppendLine("<style type=\"text/css\">\n:root, html {");
foreach (CssVariable v in widget.CssVariables)
cssOverrides.AppendLine($" --{v.Name}: {v.Value} !important;");
cssOverrides.AppendLine("}\n</style>");

if (html.Contains("</head>", StringComparison.OrdinalIgnoreCase))
html = html.Replace("</head>", cssOverrides + "\n</head>",
html = html.Replace("</head>", clientScript + "\n" + cssOverrides + "\n</head>",
StringComparison.OrdinalIgnoreCase);
else
html += cssOverrides;
html = clientScript + "\n" + html + cssOverrides;

var jsOverrides = new StringBuilder();
jsOverrides.AppendLine("\n<script>\n");
Expand Down
468 changes: 345 additions & 123 deletions SubathonManager.Server/WebServer.WebSocket.cs

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions SubathonManager.Tests/ServerUnitTests/WebServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,56 @@ await File.WriteAllTextAsync(tempHtml, "<html><head></head><body></body></html>"
AppServices.Provider = null!;
}

[Fact]
public async Task HandleWidgetRequestAsync_Puts_Client_Script_Before_A_Fragment_Widgets_Script() {
WebServer server = CreateServer();
SetupServices();
var route = new Route { Name = "TestRoute" };

string tempHtml = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".html");
await File.WriteAllTextAsync(tempHtml,
"<div id=\"w\"></div>\n<script>\nwindow.__probe = window.Subathon;\n</script>",
TestContext.Current.CancellationToken);

var widget = new Widget("Widget1", tempHtml) {
Route = route,
RouteId = route.Id,
JsVariables = new List<JsVariable> {
new() { Name = "testVar", Value = "42", Type = WidgetVariableType.Int, WidgetId = Guid.NewGuid() }
}
};
route.Widgets.Add(widget);

await using AppDbContext db = await server._factory.CreateDbContextAsync(TestContext.Current.CancellationToken);
db.Routes.Add(route);
db.Widgets.Add(widget);
await db.SaveChangesAsync(TestContext.Current.CancellationToken);

var ctx = new MockHttpContext {
Method = "GET",
Path = $"/widget/{widget.Id}"
};

await server.HandleWidgetRequest(ctx);

Assert.Equal(200, ctx.StatusCode);
string body = ctx.GetResponseText();

int clientAt = body.IndexOf("data-subathon-client", StringComparison.Ordinal);
int varsAt = body.IndexOf("const testVar = 42", StringComparison.Ordinal);
int widgetAt = body.IndexOf("window.__probe", StringComparison.Ordinal);

Assert.True(clientAt >= 0, "client script was not injected");
Assert.True(varsAt >= 0, "widget variables were not injected");
Assert.True(widgetAt >= 0, "widget markup was not served");

Assert.True(clientAt < widgetAt, "client script must come before the widget's own script");
Assert.True(varsAt < widgetAt, "widget variables must come before the widget's own script");

File.Delete(tempHtml);
AppServices.Provider = null!;
}

[Fact]
public async Task HandleWidgetRequest_Returns_404_For_Invalid_Widget() {
WebServer server = CreateServer();
Expand Down
68 changes: 68 additions & 0 deletions SubathonManager.Tests/ServerUnitTests/WebServerWebSocketTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,74 @@ public void Generate_Injection_Script() {
AppServices.Provider = null!;
}

[Fact]
public void Injection_Script_Widget_Mode_Is_Not_An_Overlay_Host() {
SetupServices();
WebServer server = CreateServer();
string script = server.GetWebsocketInjectionScript();

Assert.Contains("var IS_OVERLAY = false;", script);
Assert.Contains("var ROUTE_ID = '';", script);
Assert.DoesNotContain("__IS_OVERLAY__", script);
Assert.DoesNotContain("__ROUTE_ID__", script);
Assert.DoesNotContain("__WS_URL__", script);
Assert.DoesNotContain("__EMPTY_GUID__", script);

AppServices.Provider = null!;
}

[Fact]
public void Injection_Script_Overlay_Mode_Carries_Route_Id() {
SetupServices();
WebServer server = CreateServer();
var routeId = Guid.NewGuid();
string script = server.GetWebsocketInjectionScript(routeId.ToString());

Assert.Contains("var IS_OVERLAY = true;", script);
Assert.Contains($"var ROUTE_ID = '{routeId}';", script);
Assert.Contains($"var EMPTY_GUID = '{Guid.Empty}';", script);
Assert.Contains("ws_type: 'Overlay'", script);
Assert.Contains("ws_type: 'Widget'", script);

AppServices.Provider = null!;
}

[Fact]
public void Injection_Script_Still_Dispatches_Every_Legacy_Global() {
SetupServices();
WebServer server = CreateServer();
string script = server.GetWebsocketInjectionScript();
foreach (string handler in new[] {
"handleSubathonUpdate", "handleSubathonEvent", "handlePromptUpdate",
"handleGoalsUpdate", "handleGoalCompleted", "handleValueConfig",
"handleTotalsUpdate", "handleSubscriptionTotalsUpdate", "handleWheelSpinResult",
"handleWheelData", "handleWheelSpinStart", "handleWheelSpinStatus",
"handleVarsUpdate", "handleSubathonDisconnect"
})
Assert.Contains(handler, script);

AppServices.Provider = null!;
}

[Fact]
public void Injection_Script_Replays_Cached_State_To_Late_Legacy_Handlers() {
SetupServices();
WebServer server = CreateServer();
string script = server.GetWebsocketInjectionScript();

Assert.Contains("DOMContentLoaded", script);
Assert.Contains("replayStateToLegacy", script);
Assert.Contains("legacyServed", script);

foreach (string stateType in new[] {
"subathon_timer", "subathon_totals", "subscription_totals",
"goals_list", "value_config", "prompt_update", "wheel_data"
})
Assert.Contains(stateType + ":", script);

AppServices.Provider = null!;
}

[Fact]
public async Task Non_WebSocket_Request_Is_Rejected_As_WebSocket() {
var ctx = new MockHttpContext {
Expand Down
2 changes: 2 additions & 0 deletions SubathonManager.Tests/SubathonManager.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

<ItemGroup>

<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />

<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" Version="*"/>
<PackageReference Include="Microsoft.Testing.Extensions.TrxReport" Version="*"/>
<PackageReference Include="Microsoft.Testing.Extensions.CrashDump" Version="*"/>
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading