From 8663eb98b82ed6a459f2bb3109702aa80998978d Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 23 Sep 2026 11:04:41 +0200 Subject: [PATCH 1/3] style: format with csharpier --- .config/dotnet-tools.json | 13 + .editorconfig | 44 ++- .github/workflows/build.yml | 5 + Directory.Build.props | 4 +- Directory.Build.targets | 8 +- .../KoFi.Client.Sample.csproj | 52 ++-- samples/KoFi.Client.Sample/Program.cs | 278 +++++++++++------- .../HttpContextWebhookRequestMapper.cs | 9 +- .../KoFi.Client.AspNetCore.csproj | 65 ++-- .../KoFiEndpointRouteBuilderExtensions.cs | 132 +++++---- .../KoFiWebhookEndpointOptions.cs | 15 +- .../WebhookResponseHttpContextWriter.cs | 15 +- .../KoFi.Client.DependencyInjection.csproj | 5 +- .../KoFiClientServiceCollectionExtensions.cs | 2 +- .../Abstractions/IKoFiWebhookHandler.cs | 5 +- src/KoFi.Client/Events/KoFiCommerceEvent.cs | 2 +- src/KoFi.Client/Events/KoFiCommissionEvent.cs | 2 +- src/KoFi.Client/Events/KoFiDonationEvent.cs | 2 +- src/KoFi.Client/Events/KoFiReferralEvent.cs | 2 +- src/KoFi.Client/Events/KoFiShopOrderEvent.cs | 2 +- .../Events/KoFiSubscriptionRenewedEvent.cs | 2 +- .../Events/KoFiSubscriptionStartedEvent.cs | 2 +- src/KoFi.Client/Events/KoFiSupporterEvent.cs | 2 +- .../Events/KoFiUnknownWebhookEvent.cs | 2 +- src/KoFi.Client/Events/KoFiWebhookEvent.cs | 2 +- .../Internal/ConstantTimeStringComparer.cs | 5 +- .../Json/KoFiJsonSerializerContext.cs | 7 +- src/KoFi.Client/KoFi.Client.csproj | 52 ++-- src/KoFi.Client/Models/KoFiPayload.cs | 2 +- src/KoFi.Client/Models/KoFiShipping.cs | 2 +- src/KoFi.Client/Models/KoFiShopItem.cs | 2 +- src/KoFi.Client/Options/KoFiWebhookOptions.cs | 2 +- .../Webhooks/KoFiWebhookHandler.cs | 257 ++++++++++------ .../KoFi.Client.Tests.csproj | 5 - .../Webhooks/KoFiWebhookHandlerTests.cs | 123 +++++--- 35 files changed, 701 insertions(+), 428 deletions(-) create mode 100644 .config/dotnet-tools.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..9e028ef --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.3.0", + "commands": [ + "csharpier" + ], + "rollForward": false + } + } +} diff --git a/.editorconfig b/.editorconfig index 6c81757..e872bb7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,46 @@ -[*.cs] +[*.cs] # IDE0028: Simplify collection initialization dotnet_diagnostic.IDE0028.severity = none + +# Formatting follows CSharpier; `dotnet csharpier check` enforces it in CI. These keys keep the IDE +# formatter consistent with it, and IDE0055 is off because some layouts, such as a multi-line +# constructor initializer before an empty body, cannot be expressed to it. +dotnet_diagnostic.IDE0055.severity = none +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9ded5c..2ee11ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,6 +47,11 @@ jobs: - name: Restore dependencies run: dotnet restore KoFi.Client.slnx + - name: Check formatting + run: | + dotnet tool restore + dotnet csharpier check . + - name: Build solution run: dotnet build KoFi.Client.slnx --configuration Release --no-restore diff --git a/Directory.Build.props b/Directory.Build.props index ccd07a5..209f74c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -21,8 +21,8 @@ https://github.com/Agash/KoFi.Client ko-fi;kofi;donations;creator-economy;webhooks;api-client;dotnet;csharp;aot;nativeaot https://github.com/Agash/KoFi.Client - true + true README.md - diff --git a/Directory.Build.targets b/Directory.Build.targets index 023a0bc..ff38400 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,7 +1,13 @@ - + diff --git a/samples/KoFi.Client.Sample/KoFi.Client.Sample.csproj b/samples/KoFi.Client.Sample/KoFi.Client.Sample.csproj index f60baa6..8205418 100644 --- a/samples/KoFi.Client.Sample/KoFi.Client.Sample.csproj +++ b/samples/KoFi.Client.Sample/KoFi.Client.Sample.csproj @@ -1,30 +1,30 @@  - - net10.0 - Exe - enable - enable - preview - true - false - false - false - $(NoWarn);CA1305 - KoFi.Client.Sample - + + net10.0 + Exe + enable + enable + preview + true + false + false + false + $(NoWarn);CA1305 + KoFi.Client.Sample + - - - - - + + + + + - - - - + + + + - - - - \ No newline at end of file + + + + diff --git a/samples/KoFi.Client.Sample/Program.cs b/samples/KoFi.Client.Sample/Program.cs index a4a48e4..e6b0f9c 100644 --- a/samples/KoFi.Client.Sample/Program.cs +++ b/samples/KoFi.Client.Sample/Program.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using DevTunnels.Client; using DevTunnels.Client.Authentication; using DevTunnels.Client.Hosting; @@ -10,7 +11,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Spectre.Console; -using System.Collections.Concurrent; CancellationTokenSource shutdown = new(); @@ -45,11 +45,11 @@ public static async Task RunAsync(CancellationToken cancellationToken) { AnsiConsole.Clear(); - AnsiConsole.Write( - new FigletText("KoFi Sample") - .Color(Color.HotPink)); + AnsiConsole.Write(new FigletText("KoFi Sample").Color(Color.HotPink)); - AnsiConsole.MarkupLine("[grey]Transport-neutral Ko-fi webhook sample with ASP.NET Core, Spectre.Console, and DevTunnels.Client.[/]"); + AnsiConsole.MarkupLine( + "[grey]Transport-neutral Ko-fi webhook sample with ASP.NET Core, Spectre.Console, and DevTunnels.Client.[/]" + ); AnsiConsole.WriteLine(); SampleConfiguration configuration = PromptConfiguration(); @@ -66,10 +66,13 @@ public static async Task RunAsync(CancellationToken cancellationToken) _ = app.MapGet( "/", - () => Results.Text( - "KoFi.Client.Sample is running.\n" + - "POST Ko-fi webhook payloads to the configured route.\n", - "text/plain")); + () => + Results.Text( + "KoFi.Client.Sample is running.\n" + + "POST Ko-fi webhook payloads to the configured route.\n", + "text/plain" + ) + ); _ = app.MapKoFiWebhook( configuration.WebhookPath, @@ -89,7 +92,8 @@ public static async Task RunAsync(CancellationToken cancellationToken) { lock (consoleLock) { - string remoteIp = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + string remoteIp = + httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; string requestId = httpContext.TraceIdentifier; string auth = result.IsAuthenticated ? "[green]yes[/]" : "[red]no[/]"; @@ -97,16 +101,20 @@ public static async Task RunAsync(CancellationToken cancellationToken) string status = $"[blue]{result.Response.StatusCode}[/]"; AnsiConsole.MarkupLineInterpolated( - $"[grey]Request[/] [white]{Markup.Escape(requestId)}[/] from [white]{Markup.Escape(remoteIp)}[/] -> status {status}, authenticated {auth}, known event {known}."); + $"[grey]Request[/] [white]{Markup.Escape(requestId)}[/] from [white]{Markup.Escape(remoteIp)}[/] -> status {status}, authenticated {auth}, known event {known}." + ); if (!string.IsNullOrWhiteSpace(result.FailureReason)) { - AnsiConsole.MarkupLineInterpolated($"[yellow]Reason:[/] {Markup.Escape(result.FailureReason)}"); + AnsiConsole.MarkupLineInterpolated( + $"[yellow]Reason:[/] {Markup.Escape(result.FailureReason)}" + ); } } await Task.CompletedTask.ConfigureAwait(false); - }); + } + ); string localBaseUrl = $"http://127.0.0.1:{configuration.LocalPort}"; @@ -118,13 +126,20 @@ public static async Task RunAsync(CancellationToken cancellationToken) if (configuration.UseDevTunnels) { - devTunnelsRuntime = await StartDevTunnelsAsync(configuration, cancellationToken).ConfigureAwait(false); + devTunnelsRuntime = await StartDevTunnelsAsync(configuration, cancellationToken) + .ConfigureAwait(false); RenderTunnelSummary(configuration, devTunnelsRuntime.PublicBaseUrl); } RenderUsageInstructions(configuration, localBaseUrl, devTunnelsRuntime?.PublicBaseUrl); - await RunCommandLoopAsync(configuration, receivedEvents, devTunnelsRuntime, consoleLock, cancellationToken) + await RunCommandLoopAsync( + configuration, + receivedEvents, + devTunnelsRuntime, + consoleLock, + cancellationToken + ) .ConfigureAwait(false); if (devTunnelsRuntime is not null) @@ -142,14 +157,18 @@ private static SampleConfiguration PromptConfiguration() new TextPrompt("Local [green]HTTP port[/]?") .DefaultValue(5073) .ValidationErrorMessage("[red]Please enter a valid port.[/]") - .Validate(port => port is > 0 and <= 65535 - ? ValidationResult.Success() - : ValidationResult.Error("[red]Port must be between 1 and 65535.[/]"))); + .Validate(port => + port is > 0 and <= 65535 + ? ValidationResult.Success() + : ValidationResult.Error("[red]Port must be between 1 and 65535.[/]") + ) + ); string webhookPath = AnsiConsole.Prompt( new TextPrompt("Webhook [green]path[/]?") .DefaultValue("/webhooks/kofi/events") - .AllowEmpty()); + .AllowEmpty() + ); if (string.IsNullOrWhiteSpace(webhookPath)) { @@ -164,9 +183,13 @@ private static SampleConfiguration PromptConfiguration() string verificationToken = AnsiConsole.Prompt( new TextPrompt("Ko-fi [green]verification token[/]?") .PromptStyle("hotpink") - .Secret()); + .Secret() + ); - bool useDevTunnels = AnsiConsole.Confirm("Use [green]Azure Dev Tunnels[/] for a public HTTPS URL?", true); + bool useDevTunnels = AnsiConsole.Confirm( + "Use [green]Azure Dev Tunnels[/] for a public HTTPS URL?", + true + ); string tunnelId = "kofi-client-sample"; LoginProvider loginProvider = LoginProvider.GitHub; @@ -176,7 +199,8 @@ private static SampleConfiguration PromptConfiguration() tunnelId = AnsiConsole.Prompt( new TextPrompt("Dev Tunnel [green]tunnel ID[/]?") .DefaultValue("kofi-client-sample") - .AllowEmpty()); + .AllowEmpty() + ); if (string.IsNullOrWhiteSpace(tunnelId)) { @@ -186,7 +210,8 @@ private static SampleConfiguration PromptConfiguration() loginProvider = AnsiConsole.Prompt( new SelectionPrompt() .Title("Login provider for [green]devtunnel[/]?") - .AddChoices(LoginProvider.GitHub, LoginProvider.Microsoft)); + .AddChoices(LoginProvider.GitHub, LoginProvider.Microsoft) + ); } return new SampleConfiguration( @@ -195,71 +220,88 @@ private static SampleConfiguration PromptConfiguration() VerificationToken: verificationToken, UseDevTunnels: useDevTunnels, TunnelId: tunnelId, - LoginProvider: loginProvider); + LoginProvider: loginProvider + ); } private static async Task StartDevTunnelsAsync( SampleConfiguration configuration, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { AnsiConsole.WriteLine(); AnsiConsole.MarkupLine("[bold]Azure Dev Tunnels walkthrough[/]"); AnsiConsole.WriteLine(); - DevTunnelsClient client = new(new DevTunnelsClientOptions - { - CommandTimeout = TimeSpan.FromSeconds(20), - }); + DevTunnelsClient client = new( + new DevTunnelsClientOptions { CommandTimeout = TimeSpan.FromSeconds(20) } + ); - DevTunnelCliProbeResult probe = await client.ProbeCliAsync(cancellationToken).ConfigureAwait(false); + DevTunnelCliProbeResult probe = await client + .ProbeCliAsync(cancellationToken) + .ConfigureAwait(false); if (!probe.IsInstalled) { throw new InvalidOperationException( - "The devtunnel CLI is not installed or could not be found. " + - "Install it first, then re-run the sample."); + "The devtunnel CLI is not installed or could not be found. " + + "Install it first, then re-run the sample." + ); } - AnsiConsole.MarkupLineInterpolated($"[green]CLI found:[/] devtunnel [white]{Markup.Escape(probe.Version?.ToString() ?? "unknown")}[/]"); + AnsiConsole.MarkupLineInterpolated( + $"[green]CLI found:[/] devtunnel [white]{Markup.Escape(probe.Version?.ToString() ?? "unknown")}[/]" + ); AnsiConsole.MarkupLineInterpolated( - $"[grey]Ensuring login with[/] [white]{Markup.Escape(configuration.LoginProvider.ToString())}[/][grey]...[/]"); + $"[grey]Ensuring login with[/] [white]{Markup.Escape(configuration.LoginProvider.ToString())}[/][grey]...[/]" + ); - _ = await client.EnsureLoggedInAsync(configuration.LoginProvider, cancellationToken).ConfigureAwait(false); + _ = await client + .EnsureLoggedInAsync(configuration.LoginProvider, cancellationToken) + .ConfigureAwait(false); AnsiConsole.MarkupLine("[green]Login confirmed.[/]"); - _ = await client.CreateOrUpdateTunnelAsync( - configuration.TunnelId, - new DevTunnelOptions - { - Description = "KoFi.Client.Sample tunnel", - AllowAnonymous = true, - }, - cancellationToken).ConfigureAwait(false); + _ = await client + .CreateOrUpdateTunnelAsync( + configuration.TunnelId, + new DevTunnelOptions + { + Description = "KoFi.Client.Sample tunnel", + AllowAnonymous = true, + }, + cancellationToken + ) + .ConfigureAwait(false); - _ = await client.CreateOrReplacePortAsync( - configuration.TunnelId, - configuration.LocalPort, - new DevTunnelPortOptions - { - Protocol = "http", - }, - cancellationToken).ConfigureAwait(false); + _ = await client + .CreateOrReplacePortAsync( + configuration.TunnelId, + configuration.LocalPort, + new DevTunnelPortOptions { Protocol = "http" }, + cancellationToken + ) + .ConfigureAwait(false); - IDevTunnelHostSession session = await client.StartHostSessionAsync( - new DevTunnelHostStartOptions - { - TunnelId = configuration.TunnelId, - }, - cancellationToken).ConfigureAwait(false); + IDevTunnelHostSession session = await client + .StartHostSessionAsync( + new DevTunnelHostStartOptions { TunnelId = configuration.TunnelId }, + cancellationToken + ) + .ConfigureAwait(false); await session.WaitForReadyAsync(cancellationToken).ConfigureAwait(false); - Uri publicBaseUrl = session.PublicUrl - ?? throw new InvalidOperationException("The Dev Tunnel host session became ready without a public URL."); + Uri publicBaseUrl = + session.PublicUrl + ?? throw new InvalidOperationException( + "The Dev Tunnel host session became ready without a public URL." + ); - AnsiConsole.MarkupLineInterpolated($"[green]Tunnel ready:[/] [link]{Markup.Escape(publicBaseUrl.ToString())}[/]"); + AnsiConsole.MarkupLineInterpolated( + $"[green]Tunnel ready:[/] [link]{Markup.Escape(publicBaseUrl.ToString())}[/]" + ); return new DevTunnelsRuntime(session, publicBaseUrl); } @@ -278,17 +320,25 @@ private static void RenderStartupSummary(SampleConfiguration configuration, stri _ = table.AddRow("Webhook path", $"[white]{Markup.Escape(configuration.WebhookPath)}[/]"); _ = table.AddRow("Local webhook URL", $"[white]{Markup.Escape(localWebhookUrl)}[/]"); _ = table.AddRow("Verification token", "[grey](hidden)[/]"); - _ = table.AddRow("Dev Tunnels enabled", configuration.UseDevTunnels ? "[green]yes[/]" : "[yellow]no[/]"); + _ = table.AddRow( + "Dev Tunnels enabled", + configuration.UseDevTunnels ? "[green]yes[/]" : "[yellow]no[/]" + ); - AnsiConsole.Write(new Panel(table) - .Header("[bold]Local runtime[/]") - .Border(BoxBorder.Rounded) - .BorderColor(Color.HotPink)); + AnsiConsole.Write( + new Panel(table) + .Header("[bold]Local runtime[/]") + .Border(BoxBorder.Rounded) + .BorderColor(Color.HotPink) + ); } private static void RenderTunnelSummary(SampleConfiguration configuration, Uri publicBaseUrl) { - string publicWebhookUrl = CombineUrl(publicBaseUrl.ToString().TrimEnd('/'), configuration.WebhookPath); + string publicWebhookUrl = CombineUrl( + publicBaseUrl.ToString().TrimEnd('/'), + configuration.WebhookPath + ); Table table = new Table() .RoundedBorder() @@ -300,16 +350,19 @@ private static void RenderTunnelSummary(SampleConfiguration configuration, Uri p _ = table.AddRow("Public base URL", $"[white]{Markup.Escape(publicBaseUrl.ToString())}[/]"); _ = table.AddRow("Public webhook URL", $"[white]{Markup.Escape(publicWebhookUrl)}[/]"); - AnsiConsole.Write(new Panel(table) - .Header("[bold]Public tunnel[/]") - .Border(BoxBorder.Rounded) - .BorderColor(Color.Green)); + AnsiConsole.Write( + new Panel(table) + .Header("[bold]Public tunnel[/]") + .Border(BoxBorder.Rounded) + .BorderColor(Color.Green) + ); } private static void RenderUsageInstructions( SampleConfiguration configuration, string localBaseUrl, - Uri? publicBaseUrl) + Uri? publicBaseUrl + ) { string localWebhookUrl = CombineUrl(localBaseUrl, configuration.WebhookPath); string? publicWebhookUrl = publicBaseUrl is null @@ -326,15 +379,20 @@ private static void RenderUsageInstructions( new Text(string.Empty), new Markup($"[grey]Local webhook URL:[/] [white]{Markup.Escape(localWebhookUrl)}[/]"), publicWebhookUrl is not null - ? new Markup($"[grey]Public webhook URL:[/] [white]{Markup.Escape(publicWebhookUrl)}[/]") + ? new Markup( + $"[grey]Public webhook URL:[/] [white]{Markup.Escape(publicWebhookUrl)}[/]" + ) : new Markup("[grey]Public webhook URL:[/] [yellow](Dev Tunnels disabled)[/]"), new Text(string.Empty), - new Markup("[grey]Commands are available below while the sample is running.[/]")); + new Markup("[grey]Commands are available below while the sample is running.[/]") + ); - AnsiConsole.Write(new Panel(rows) - .Header("[bold]How to use the sample[/]") - .Border(BoxBorder.Rounded) - .BorderColor(Color.Blue)); + AnsiConsole.Write( + new Panel(rows) + .Header("[bold]How to use the sample[/]") + .Border(BoxBorder.Rounded) + .BorderColor(Color.Blue) + ); } private static async Task RunCommandLoopAsync( @@ -342,7 +400,8 @@ private static async Task RunCommandLoopAsync( ConcurrentQueue receivedEvents, DevTunnelsRuntime? devTunnelsRuntime, object consoleLock, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { while (!cancellationToken.IsCancellationRequested) { @@ -355,7 +414,9 @@ private static async Task RunCommandLoopAsync( "Show webhook URLs", "Show recent events", "Show sample payload hint", - "Exit")); + "Exit" + ) + ); switch (command) { @@ -363,7 +424,10 @@ private static async Task RunCommandLoopAsync( lock (consoleLock) { string localBaseUrl = $"http://127.0.0.1:{configuration.LocalPort}"; - string localWebhookUrl = CombineUrl(localBaseUrl, configuration.WebhookPath); + string localWebhookUrl = CombineUrl( + localBaseUrl, + configuration.WebhookPath + ); Table table = new Table() .RoundedBorder() @@ -376,9 +440,13 @@ private static async Task RunCommandLoopAsync( { string publicWebhookUrl = CombineUrl( devTunnelsRuntime.PublicBaseUrl.ToString().TrimEnd('/'), - configuration.WebhookPath); + configuration.WebhookPath + ); - _ = table.AddRow("Public", $"[white]{Markup.Escape(publicWebhookUrl)}[/]"); + _ = table.AddRow( + "Public", + $"[white]{Markup.Escape(publicWebhookUrl)}[/]" + ); } AnsiConsole.Write(table); @@ -412,7 +480,8 @@ private static async Task RunCommandLoopAsync( Markup.Escape(evt.FromName ?? "(anonymous)"), evt.Amount.ToString("0.00"), Markup.Escape(evt.Currency), - Markup.Escape(evt.Timestamp.ToString("u"))); + Markup.Escape(evt.Timestamp.ToString("u")) + ); } AnsiConsole.Write(table); @@ -423,10 +492,12 @@ private static async Task RunCommandLoopAsync( case "Show sample payload hint": lock (consoleLock) { - AnsiConsole.Write(new Panel(GetSamplePayloadHint(configuration.VerificationToken)) - .Header("[bold]Example Ko-fi form body[/]") - .Border(BoxBorder.Rounded) - .BorderColor(Color.Yellow)); + AnsiConsole.Write( + new Panel(GetSamplePayloadHint(configuration.VerificationToken)) + .Header("[bold]Example Ko-fi form body[/]") + .Border(BoxBorder.Rounded) + .BorderColor(Color.Yellow) + ); } break; @@ -457,10 +528,12 @@ private static void RenderReceivedEvent(KoFiWebhookEvent evt) _ = grid.AddRow("[bold]Message[/]", Markup.Escape(evt.Message)); } - AnsiConsole.Write(new Panel(grid) - .Header("[bold green]Webhook event received[/]") - .Border(BoxBorder.Rounded) - .BorderColor(Color.Green)); + AnsiConsole.Write( + new Panel(grid) + .Header("[bold green]Webhook event received[/]") + .Border(BoxBorder.Rounded) + .BorderColor(Color.Green) + ); } private static string GetEventDisplayName(KoFiWebhookEvent evt) @@ -487,14 +560,15 @@ private static string CombineUrl(string baseUrl, string path) private static string GetSamplePayloadHint(string verificationToken) { return """ - Ko-fi posts application/x-www-form-urlencoded with a single field named data. + Ko-fi posts application/x-www-form-urlencoded with a single field named data. - Example form field value: - data={"verification_token":"REPLACE_TOKEN","message_id":"sample-message-id","timestamp":"2026-03-17T12:00:00Z","type":"Donation","is_public":true,"from_name":"Sample Supporter","message":"Hello from the sample","amount":"5.00","url":"https://ko-fi.com","email":"supporter@example.com","currency":"EUR","is_subscription_payment":false,"is_first_subscription_payment":false,"kofi_transaction_id":"txn_123","tier_name":null,"discord_username":null,"discord_user_id":null} + Example form field value: + data={"verification_token":"REPLACE_TOKEN","message_id":"sample-message-id","timestamp":"2026-03-17T12:00:00Z","type":"Donation","is_public":true,"from_name":"Sample Supporter","message":"Hello from the sample","amount":"5.00","url":"https://ko-fi.com","email":"supporter@example.com","currency":"EUR","is_subscription_payment":false,"is_first_subscription_payment":false,"kofi_transaction_id":"txn_123","tier_name":null,"discord_username":null,"discord_user_id":null} - Replace REPLACE_TOKEN with the configured verification token: - """ - + Environment.NewLine + verificationToken; + Replace REPLACE_TOKEN with the configured verification token: + """ + + Environment.NewLine + + verificationToken; } private sealed record SampleConfiguration( @@ -503,18 +577,16 @@ private sealed record SampleConfiguration( string VerificationToken, bool UseDevTunnels, string TunnelId, - LoginProvider LoginProvider); + LoginProvider LoginProvider + ); /// /// Initializes a new instance of the class. /// /// The live host session returned by the client. /// The public base URL exposed by the tunnel. - private sealed class DevTunnelsRuntime( - dynamic session, - Uri publicBaseUrl) + private sealed class DevTunnelsRuntime(dynamic session, Uri publicBaseUrl) { - /// /// Gets the active host session. /// @@ -543,4 +615,4 @@ public async Task StopAsync(CancellationToken cancellationToken) } } } -} \ No newline at end of file +} diff --git a/src/KoFi.Client.AspNetCore/HttpContextWebhookRequestMapper.cs b/src/KoFi.Client.AspNetCore/HttpContextWebhookRequestMapper.cs index a5154a8..33653b0 100644 --- a/src/KoFi.Client.AspNetCore/HttpContextWebhookRequestMapper.cs +++ b/src/KoFi.Client.AspNetCore/HttpContextWebhookRequestMapper.cs @@ -22,7 +22,8 @@ public static class HttpContextWebhookRequestMapper /// public static async Task FromHttpContextAsync( HttpContext context, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { ArgumentNullException.ThrowIfNull(context); @@ -55,10 +56,12 @@ public static async Task FromHttpContextAsync( { Method = context.Request.Method, Path = context.Request.Path.HasValue ? context.Request.Path.Value : "/", - QueryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : null, + QueryString = context.Request.QueryString.HasValue + ? context.Request.QueryString.Value + : null, ContentType = context.Request.ContentType, Headers = headers, Body = body, }; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client.AspNetCore/KoFi.Client.AspNetCore.csproj b/src/KoFi.Client.AspNetCore/KoFi.Client.AspNetCore.csproj index a8c9449..ae737b0 100644 --- a/src/KoFi.Client.AspNetCore/KoFi.Client.AspNetCore.csproj +++ b/src/KoFi.Client.AspNetCore/KoFi.Client.AspNetCore.csproj @@ -1,37 +1,36 @@  - - net9.0;net10.0;net11.0 + + net9.0;net10.0;net11.0 true - enable - enable - preview - true - true - true - KoFi.Client.AspNetCore - v - true - KoFi.Client.AspNetCore - Agash - Agash - ASP.NET Core adapter package for KoFi.Client. - MIT - https://github.com/Agash/KoFi.Client - git - false + enable + enable + preview + true + true + true + KoFi.Client.AspNetCore + v + true + KoFi.Client.AspNetCore + Agash + Agash + ASP.NET Core adapter package for KoFi.Client. + MIT + https://github.com/Agash/KoFi.Client + git + false + - + + + + - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + diff --git a/src/KoFi.Client.AspNetCore/KoFiEndpointRouteBuilderExtensions.cs b/src/KoFi.Client.AspNetCore/KoFiEndpointRouteBuilderExtensions.cs index 7d42236..66c0994 100644 --- a/src/KoFi.Client.AspNetCore/KoFiEndpointRouteBuilderExtensions.cs +++ b/src/KoFi.Client.AspNetCore/KoFiEndpointRouteBuilderExtensions.cs @@ -30,7 +30,8 @@ public static class KoFiEndpointRouteBuilderExtensions public static IEndpointConventionBuilder MapKoFiWebhook( this IEndpointRouteBuilder endpoints, string pattern, - Action configure) + Action configure + ) { ArgumentNullException.ThrowIfNull(endpoints); ArgumentException.ThrowIfNullOrEmpty(pattern); @@ -38,42 +39,50 @@ public static IEndpointConventionBuilder MapKoFiWebhook( KoFiWebhookEndpointOptions options = new() { - ResolveWebhookOptionsAsync = static (_, _) => Task.FromResult(new KoFiWebhookOptions - { - VerificationToken = string.Empty, - }), + ResolveWebhookOptionsAsync = static (_, _) => + Task.FromResult(new KoFiWebhookOptions { VerificationToken = string.Empty }), }; configure(options); - IEndpointConventionBuilder builder = endpoints.MapPost(pattern, async context => - { - IKoFiWebhookHandler handler = context.RequestServices.GetRequiredService(); - - KoFiWebhookOptions webhookOptions = - await options.ResolveWebhookOptionsAsync(context, context.RequestAborted).ConfigureAwait(false); + IEndpointConventionBuilder builder = endpoints.MapPost( + pattern, + async context => + { + IKoFiWebhookHandler handler = + context.RequestServices.GetRequiredService(); - WebhookRequest request = - await HttpContextWebhookRequestMapper.FromHttpContextAsync(context, context.RequestAborted) + KoFiWebhookOptions webhookOptions = await options + .ResolveWebhookOptionsAsync(context, context.RequestAborted) .ConfigureAwait(false); - WebhookHandleResult result = - await handler.HandleAsync(request, webhookOptions, context.RequestAborted) + WebhookRequest request = await HttpContextWebhookRequestMapper + .FromHttpContextAsync(context, context.RequestAborted) .ConfigureAwait(false); - if (result.Event is KoFiWebhookEvent evt && options.OnEventAsync is not null) - { - await options.OnEventAsync(evt, context, context.RequestAborted).ConfigureAwait(false); - } + WebhookHandleResult result = await handler + .HandleAsync(request, webhookOptions, context.RequestAborted) + .ConfigureAwait(false); - if (options.OnResultAsync is not null) - { - await options.OnResultAsync(result, context, context.RequestAborted).ConfigureAwait(false); + if (result.Event is KoFiWebhookEvent evt && options.OnEventAsync is not null) + { + await options + .OnEventAsync(evt, context, context.RequestAborted) + .ConfigureAwait(false); + } + + if (options.OnResultAsync is not null) + { + await options + .OnResultAsync(result, context, context.RequestAborted) + .ConfigureAwait(false); + } + + await WebhookResponseHttpContextWriter + .WriteAsync(context, result.Response, context.RequestAborted) + .ConfigureAwait(false); } - - await WebhookResponseHttpContextWriter.WriteAsync(context, result.Response, context.RequestAborted) - .ConfigureAwait(false); - }); + ); return builder; } @@ -100,7 +109,13 @@ public static IEndpointConventionBuilder MapKoFiWebhook( string pattern, Func> resolveWebhookOptionsAsync, Func? onEventAsync = null, - Func, HttpContext, CancellationToken, Task>? onResultAsync = null) + Func< + WebhookHandleResult, + HttpContext, + CancellationToken, + Task + >? onResultAsync = null + ) { ArgumentNullException.ThrowIfNull(resolveWebhookOptionsAsync); @@ -111,7 +126,8 @@ public static IEndpointConventionBuilder MapKoFiWebhook( options.ResolveWebhookOptionsAsync = resolveWebhookOptionsAsync; options.OnEventAsync = onEventAsync; options.OnResultAsync = onResultAsync; - }); + } + ); } /// @@ -136,43 +152,53 @@ public static IEndpointConventionBuilder MapKoFiWebhook( string pattern, string verificationToken, Func? onEventAsync = null, - Func, HttpContext, CancellationToken, Task>? onResultAsync = null) + Func< + WebhookHandleResult, + HttpContext, + CancellationToken, + Task + >? onResultAsync = null + ) { ArgumentNullException.ThrowIfNull(endpoints); ArgumentException.ThrowIfNullOrEmpty(pattern); ArgumentException.ThrowIfNullOrEmpty(verificationToken); - IEndpointConventionBuilder builder = endpoints.MapPost(pattern, async context => - { - IKoFiWebhookHandler handler = context.RequestServices.GetRequiredService(); + IEndpointConventionBuilder builder = endpoints.MapPost( + pattern, + async context => + { + IKoFiWebhookHandler handler = + context.RequestServices.GetRequiredService(); - WebhookRequest request = - await HttpContextWebhookRequestMapper.FromHttpContextAsync(context, context.RequestAborted) + WebhookRequest request = await HttpContextWebhookRequestMapper + .FromHttpContextAsync(context, context.RequestAborted) .ConfigureAwait(false); - WebhookHandleResult result = - await handler.HandleAsync( + WebhookHandleResult result = await handler + .HandleAsync( request, - new KoFiWebhookOptions - { - VerificationToken = verificationToken, - }, - context.RequestAborted) + new KoFiWebhookOptions { VerificationToken = verificationToken }, + context.RequestAborted + ) .ConfigureAwait(false); - if (result.Event is KoFiWebhookEvent evt && onEventAsync is not null) - { - await onEventAsync(evt, context, context.RequestAborted).ConfigureAwait(false); - } + if (result.Event is KoFiWebhookEvent evt && onEventAsync is not null) + { + await onEventAsync(evt, context, context.RequestAborted).ConfigureAwait(false); + } - if (onResultAsync is not null) - { - await onResultAsync(result, context, context.RequestAborted).ConfigureAwait(false); - } + if (onResultAsync is not null) + { + await onResultAsync(result, context, context.RequestAborted) + .ConfigureAwait(false); + } - await WebhookResponseHttpContextWriter.WriteAsync(context, result.Response, context.RequestAborted) - .ConfigureAwait(false); - }); + await WebhookResponseHttpContextWriter + .WriteAsync(context, result.Response, context.RequestAborted) + .ConfigureAwait(false); + } + ); _ = builder.WithMetadata(new KoFiFixedVerificationTokenMetadata(verificationToken)); @@ -180,4 +206,4 @@ await WebhookResponseHttpContextWriter.WriteAsync(context, result.Response, cont } private sealed record KoFiFixedVerificationTokenMetadata(string VerificationToken); -} \ No newline at end of file +} diff --git a/src/KoFi.Client.AspNetCore/KoFiWebhookEndpointOptions.cs b/src/KoFi.Client.AspNetCore/KoFiWebhookEndpointOptions.cs index 42a8533..1da6396 100644 --- a/src/KoFi.Client.AspNetCore/KoFiWebhookEndpointOptions.cs +++ b/src/KoFi.Client.AspNetCore/KoFiWebhookEndpointOptions.cs @@ -18,7 +18,11 @@ public sealed class KoFiWebhookEndpointOptions /// This is intentionally request-based so host applications can resolve tokens and other /// configuration from instance-scoped settings, route values, or custom stores. /// - public required Func> ResolveWebhookOptionsAsync { get; set; } + public required Func< + HttpContext, + CancellationToken, + Task + > ResolveWebhookOptionsAsync { get; set; } /// /// Gets or sets an optional callback invoked after a normalized Ko-fi event has been @@ -34,5 +38,10 @@ public sealed class KoFiWebhookEndpointOptions /// Gets or sets an optional callback invoked after the Ko-fi handler completes, regardless /// of whether it produced a normalized event. /// - public Func, HttpContext, CancellationToken, Task>? OnResultAsync { get; set; } -} \ No newline at end of file + public Func< + WebhookHandleResult, + HttpContext, + CancellationToken, + Task + >? OnResultAsync { get; set; } +} diff --git a/src/KoFi.Client.AspNetCore/WebhookResponseHttpContextWriter.cs b/src/KoFi.Client.AspNetCore/WebhookResponseHttpContextWriter.cs index 34d3dc9..acc64d2 100644 --- a/src/KoFi.Client.AspNetCore/WebhookResponseHttpContextWriter.cs +++ b/src/KoFi.Client.AspNetCore/WebhookResponseHttpContextWriter.cs @@ -21,7 +21,8 @@ public static class WebhookResponseHttpContextWriter public static async Task WriteAsync( HttpContext context, WebhookResponse response, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(response); @@ -35,8 +36,10 @@ public static async Task WriteAsync( foreach ((string key, string[] values) in response.Headers) { - if (string.Equals(key, "Content-Type", StringComparison.OrdinalIgnoreCase) || - string.Equals(key, "Content-Length", StringComparison.OrdinalIgnoreCase)) + if ( + string.Equals(key, "Content-Type", StringComparison.OrdinalIgnoreCase) + || string.Equals(key, "Content-Length", StringComparison.OrdinalIgnoreCase) + ) { continue; } @@ -47,11 +50,13 @@ public static async Task WriteAsync( if (response.Body is { Length: > 0 }) { context.Response.ContentLength = response.Body.Length; - await context.Response.Body.WriteAsync(response.Body, cancellationToken).ConfigureAwait(false); + await context + .Response.Body.WriteAsync(response.Body, cancellationToken) + .ConfigureAwait(false); } else { context.Response.ContentLength = 0; } } -} \ No newline at end of file +} diff --git a/src/KoFi.Client.DependencyInjection/KoFi.Client.DependencyInjection.csproj b/src/KoFi.Client.DependencyInjection/KoFi.Client.DependencyInjection.csproj index e5135e4..7f109ac 100644 --- a/src/KoFi.Client.DependencyInjection/KoFi.Client.DependencyInjection.csproj +++ b/src/KoFi.Client.DependencyInjection/KoFi.Client.DependencyInjection.csproj @@ -18,7 +18,10 @@ - + diff --git a/src/KoFi.Client.DependencyInjection/KoFiClientServiceCollectionExtensions.cs b/src/KoFi.Client.DependencyInjection/KoFiClientServiceCollectionExtensions.cs index b8ece34..0981ccb 100644 --- a/src/KoFi.Client.DependencyInjection/KoFiClientServiceCollectionExtensions.cs +++ b/src/KoFi.Client.DependencyInjection/KoFiClientServiceCollectionExtensions.cs @@ -22,4 +22,4 @@ public static IServiceCollection AddKoFiClient(this IServiceCollection services) services.TryAddSingleton(); return services; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Abstractions/IKoFiWebhookHandler.cs b/src/KoFi.Client/Abstractions/IKoFiWebhookHandler.cs index 52d1ae7..2ea7621 100644 --- a/src/KoFi.Client/Abstractions/IKoFiWebhookHandler.cs +++ b/src/KoFi.Client/Abstractions/IKoFiWebhookHandler.cs @@ -31,5 +31,6 @@ public interface IKoFiWebhookHandler Task> HandleAsync( WebhookRequest request, KoFiWebhookOptions options, - CancellationToken cancellationToken = default); -} \ No newline at end of file + CancellationToken cancellationToken = default + ); +} diff --git a/src/KoFi.Client/Events/KoFiCommerceEvent.cs b/src/KoFi.Client/Events/KoFiCommerceEvent.cs index 50354b8..227e677 100644 --- a/src/KoFi.Client/Events/KoFiCommerceEvent.cs +++ b/src/KoFi.Client/Events/KoFiCommerceEvent.cs @@ -20,4 +20,4 @@ public abstract record KoFiCommerceEvent : KoFiWebhookEvent /// Gets the shipping address, if present. /// public KoFiShipping? Shipping { get; init; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Events/KoFiCommissionEvent.cs b/src/KoFi.Client/Events/KoFiCommissionEvent.cs index a5e343b..4fb1f91 100644 --- a/src/KoFi.Client/Events/KoFiCommissionEvent.cs +++ b/src/KoFi.Client/Events/KoFiCommissionEvent.cs @@ -3,4 +3,4 @@ /// /// Represents a Ko-fi commission event. /// -public sealed record KoFiCommissionEvent : KoFiCommerceEvent; \ No newline at end of file +public sealed record KoFiCommissionEvent : KoFiCommerceEvent; diff --git a/src/KoFi.Client/Events/KoFiDonationEvent.cs b/src/KoFi.Client/Events/KoFiDonationEvent.cs index 79ca964..209202a 100644 --- a/src/KoFi.Client/Events/KoFiDonationEvent.cs +++ b/src/KoFi.Client/Events/KoFiDonationEvent.cs @@ -3,4 +3,4 @@ /// /// Represents a Ko-fi donation event. /// -public sealed record KoFiDonationEvent : KoFiSupporterEvent; \ No newline at end of file +public sealed record KoFiDonationEvent : KoFiSupporterEvent; diff --git a/src/KoFi.Client/Events/KoFiReferralEvent.cs b/src/KoFi.Client/Events/KoFiReferralEvent.cs index eed0203..c26f201 100644 --- a/src/KoFi.Client/Events/KoFiReferralEvent.cs +++ b/src/KoFi.Client/Events/KoFiReferralEvent.cs @@ -3,4 +3,4 @@ /// /// Represents a Ko-fi referral event. /// -public sealed record KoFiReferralEvent : KoFiSupporterEvent; \ No newline at end of file +public sealed record KoFiReferralEvent : KoFiSupporterEvent; diff --git a/src/KoFi.Client/Events/KoFiShopOrderEvent.cs b/src/KoFi.Client/Events/KoFiShopOrderEvent.cs index 9e9ed2d..98e62fe 100644 --- a/src/KoFi.Client/Events/KoFiShopOrderEvent.cs +++ b/src/KoFi.Client/Events/KoFiShopOrderEvent.cs @@ -3,4 +3,4 @@ /// /// Represents a Ko-fi shop order event. /// -public sealed record KoFiShopOrderEvent : KoFiCommerceEvent; \ No newline at end of file +public sealed record KoFiShopOrderEvent : KoFiCommerceEvent; diff --git a/src/KoFi.Client/Events/KoFiSubscriptionRenewedEvent.cs b/src/KoFi.Client/Events/KoFiSubscriptionRenewedEvent.cs index b057612..83b8f6a 100644 --- a/src/KoFi.Client/Events/KoFiSubscriptionRenewedEvent.cs +++ b/src/KoFi.Client/Events/KoFiSubscriptionRenewedEvent.cs @@ -3,4 +3,4 @@ /// /// Represents a recurring renewal payment for an existing Ko-fi subscription. /// -public sealed record KoFiSubscriptionRenewedEvent : KoFiSupporterEvent; \ No newline at end of file +public sealed record KoFiSubscriptionRenewedEvent : KoFiSupporterEvent; diff --git a/src/KoFi.Client/Events/KoFiSubscriptionStartedEvent.cs b/src/KoFi.Client/Events/KoFiSubscriptionStartedEvent.cs index 061fb01..48ec7f3 100644 --- a/src/KoFi.Client/Events/KoFiSubscriptionStartedEvent.cs +++ b/src/KoFi.Client/Events/KoFiSubscriptionStartedEvent.cs @@ -3,4 +3,4 @@ /// /// Represents the first successful payment for a Ko-fi subscription. /// -public sealed record KoFiSubscriptionStartedEvent : KoFiSupporterEvent; \ No newline at end of file +public sealed record KoFiSubscriptionStartedEvent : KoFiSupporterEvent; diff --git a/src/KoFi.Client/Events/KoFiSupporterEvent.cs b/src/KoFi.Client/Events/KoFiSupporterEvent.cs index 296f05e..7eef700 100644 --- a/src/KoFi.Client/Events/KoFiSupporterEvent.cs +++ b/src/KoFi.Client/Events/KoFiSupporterEvent.cs @@ -13,4 +13,4 @@ public abstract record KoFiSupporterEvent : KoFiWebhookEvent /// Gets the Ko-fi membership tier name, if present. /// public string? TierName { get; init; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Events/KoFiUnknownWebhookEvent.cs b/src/KoFi.Client/Events/KoFiUnknownWebhookEvent.cs index 2dff1fc..0e240e1 100644 --- a/src/KoFi.Client/Events/KoFiUnknownWebhookEvent.cs +++ b/src/KoFi.Client/Events/KoFiUnknownWebhookEvent.cs @@ -15,4 +15,4 @@ public sealed record KoFiUnknownWebhookEvent : KoFiWebhookEvent /// Gets the raw payload as received from Ko-fi. /// public required JsonElement RawPayload { get; init; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Events/KoFiWebhookEvent.cs b/src/KoFi.Client/Events/KoFiWebhookEvent.cs index 103e881..25cde64 100644 --- a/src/KoFi.Client/Events/KoFiWebhookEvent.cs +++ b/src/KoFi.Client/Events/KoFiWebhookEvent.cs @@ -68,4 +68,4 @@ public abstract record KoFiWebhookEvent /// Gets the Discord user identifier associated with the event, if present. /// public string? DiscordUserId { get; init; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Internal/ConstantTimeStringComparer.cs b/src/KoFi.Client/Internal/ConstantTimeStringComparer.cs index 7c5472f..5fb061d 100644 --- a/src/KoFi.Client/Internal/ConstantTimeStringComparer.cs +++ b/src/KoFi.Client/Internal/ConstantTimeStringComparer.cs @@ -27,6 +27,7 @@ public static bool Equals(string? left, string? right) byte[] leftBytes = Encoding.UTF8.GetBytes(left); byte[] rightBytes = Encoding.UTF8.GetBytes(right); - return leftBytes.Length == rightBytes.Length && CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes); + return leftBytes.Length == rightBytes.Length + && CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes); } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Json/KoFiJsonSerializerContext.cs b/src/KoFi.Client/Json/KoFiJsonSerializerContext.cs index 99966da..b7b4720 100644 --- a/src/KoFi.Client/Json/KoFiJsonSerializerContext.cs +++ b/src/KoFi.Client/Json/KoFiJsonSerializerContext.cs @@ -1,5 +1,5 @@ -using KoFi.Client.Models; using System.Text.Json.Serialization; +using KoFi.Client.Models; namespace KoFi.Client.Json; @@ -13,8 +13,9 @@ namespace KoFi.Client.Json; [JsonSourceGenerationOptions( PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified, DefaultIgnoreCondition = JsonIgnoreCondition.Never, - WriteIndented = false)] + WriteIndented = false +)] [JsonSerializable(typeof(KoFiPayload))] [JsonSerializable(typeof(KoFiShopItem))] [JsonSerializable(typeof(KoFiShipping))] -internal sealed partial class KoFiJsonSerializerContext : JsonSerializerContext; \ No newline at end of file +internal sealed partial class KoFiJsonSerializerContext : JsonSerializerContext; diff --git a/src/KoFi.Client/KoFi.Client.csproj b/src/KoFi.Client/KoFi.Client.csproj index 63f944d..b84cb5c 100644 --- a/src/KoFi.Client/KoFi.Client.csproj +++ b/src/KoFi.Client/KoFi.Client.csproj @@ -1,29 +1,29 @@  - - net9.0;net10.0;net11.0 + + net9.0;net10.0;net11.0 true - enable - enable - preview - true - true - true - KoFi.Client - KoFi.Client - Agash - Agash - Typed, transport-neutral .NET client library for Ko-fi webhook processing. - MIT - https://github.com/Agash/KoFi.Client - git - false - v - true - + enable + enable + preview + true + true + true + KoFi.Client + KoFi.Client + Agash + Agash + Typed, transport-neutral .NET client library for Ko-fi webhook processing. + MIT + https://github.com/Agash/KoFi.Client + git + false + v + true + - - - - - - \ No newline at end of file + + + + + + diff --git a/src/KoFi.Client/Models/KoFiPayload.cs b/src/KoFi.Client/Models/KoFiPayload.cs index a8f6857..19ec8b9 100644 --- a/src/KoFi.Client/Models/KoFiPayload.cs +++ b/src/KoFi.Client/Models/KoFiPayload.cs @@ -132,4 +132,4 @@ public sealed record KoFiPayload /// [JsonPropertyName("discord_user_id")] public string? DiscordUserId { get; init; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Models/KoFiShipping.cs b/src/KoFi.Client/Models/KoFiShipping.cs index a28b756..40595f0 100644 --- a/src/KoFi.Client/Models/KoFiShipping.cs +++ b/src/KoFi.Client/Models/KoFiShipping.cs @@ -54,4 +54,4 @@ public sealed record KoFiShipping /// [JsonPropertyName("telephone")] public string? Telephone { get; init; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Models/KoFiShopItem.cs b/src/KoFi.Client/Models/KoFiShopItem.cs index 8690c78..336c8ea 100644 --- a/src/KoFi.Client/Models/KoFiShopItem.cs +++ b/src/KoFi.Client/Models/KoFiShopItem.cs @@ -24,4 +24,4 @@ public sealed record KoFiShopItem /// [JsonPropertyName("quantity")] public required int Quantity { get; init; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Options/KoFiWebhookOptions.cs b/src/KoFi.Client/Options/KoFiWebhookOptions.cs index 6705bff..04b6a0d 100644 --- a/src/KoFi.Client/Options/KoFiWebhookOptions.cs +++ b/src/KoFi.Client/Options/KoFiWebhookOptions.cs @@ -14,4 +14,4 @@ public sealed class KoFiWebhookOptions /// the webhook to be accepted. /// public required string VerificationToken { get; init; } -} \ No newline at end of file +} diff --git a/src/KoFi.Client/Webhooks/KoFiWebhookHandler.cs b/src/KoFi.Client/Webhooks/KoFiWebhookHandler.cs index f8a878c..6844cd5 100644 --- a/src/KoFi.Client/Webhooks/KoFiWebhookHandler.cs +++ b/src/KoFi.Client/Webhooks/KoFiWebhookHandler.cs @@ -1,13 +1,13 @@ -using Agash.Webhook.Abstractions; +using System.Globalization; +using System.Text; +using System.Text.Json; +using Agash.Webhook.Abstractions; using KoFi.Client.Abstractions; using KoFi.Client.Events; using KoFi.Client.Internal; using KoFi.Client.Json; using KoFi.Client.Models; using KoFi.Client.Options; -using System.Globalization; -using System.Text; -using System.Text.Json; namespace KoFi.Client.Webhooks; @@ -28,7 +28,8 @@ public sealed class KoFiWebhookHandler : IKoFiWebhookHandler public Task> HandleAsync( WebhookRequest request, KoFiWebhookOptions options, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(options); @@ -38,26 +39,31 @@ public Task> HandleAsync( if (!string.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(405), - IsAuthenticated = false, - IsKnownEvent = false, - Event = null, - FailureReason = "Unsupported HTTP method. Ko-fi webhooks must use POST.", - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(405), + IsAuthenticated = false, + IsKnownEvent = false, + Event = null, + FailureReason = "Unsupported HTTP method. Ko-fi webhooks must use POST.", + } + ); } if (!request.HasContentType(FormUrlEncodedContentType)) { - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(400), - IsAuthenticated = false, - IsKnownEvent = false, - Event = null, - FailureReason = "Unsupported content type. Expected application/x-www-form-urlencoded.", - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(400), + IsAuthenticated = false, + IsKnownEvent = false, + Event = null, + FailureReason = + "Unsupported content type. Expected application/x-www-form-urlencoded.", + } + ); } string formBody; @@ -67,14 +73,16 @@ public Task> HandleAsync( } catch (Exception ex) { - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(400), - IsAuthenticated = false, - IsKnownEvent = false, - Event = null, - FailureReason = $"Unable to decode request body as UTF-8: {ex.Message}", - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(400), + IsAuthenticated = false, + IsKnownEvent = false, + Event = null, + FailureReason = $"Unable to decode request body as UTF-8: {ex.Message}", + } + ); } Dictionary formValues; @@ -84,100 +92,136 @@ public Task> HandleAsync( } catch (Exception ex) { - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(400), - IsAuthenticated = false, - IsKnownEvent = false, - Event = null, - FailureReason = $"Malformed form body: {ex.Message}", - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(400), + IsAuthenticated = false, + IsKnownEvent = false, + Event = null, + FailureReason = $"Malformed form body: {ex.Message}", + } + ); } - if (!formValues.TryGetValue(DataFieldName, out string? rawJson) || string.IsNullOrWhiteSpace(rawJson)) + if ( + !formValues.TryGetValue(DataFieldName, out string? rawJson) + || string.IsNullOrWhiteSpace(rawJson) + ) { - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(400), - IsAuthenticated = false, - IsKnownEvent = false, - Event = null, - FailureReason = "The form body did not contain a non-empty 'data' field.", - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(400), + IsAuthenticated = false, + IsKnownEvent = false, + Event = null, + FailureReason = "The form body did not contain a non-empty 'data' field.", + } + ); } KoFiPayload? payload; JsonDocument? rawJsonDocument; try { - payload = JsonSerializer.Deserialize(rawJson, KoFiJsonSerializerContext.Default.KoFiPayload); + payload = JsonSerializer.Deserialize( + rawJson, + KoFiJsonSerializerContext.Default.KoFiPayload + ); rawJsonDocument = JsonDocument.Parse(rawJson); } catch (JsonException ex) { - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(400), - IsAuthenticated = false, - IsKnownEvent = false, - Event = null, - FailureReason = $"The 'data' field did not contain valid Ko-fi JSON: {ex.Message}", - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(400), + IsAuthenticated = false, + IsKnownEvent = false, + Event = null, + FailureReason = + $"The 'data' field did not contain valid Ko-fi JSON: {ex.Message}", + } + ); } if (payload is null) { - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(400), - IsAuthenticated = false, - IsKnownEvent = false, - Event = null, - FailureReason = "The Ko-fi payload could not be deserialized.", - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(400), + IsAuthenticated = false, + IsKnownEvent = false, + Event = null, + FailureReason = "The Ko-fi payload could not be deserialized.", + } + ); } - if (!ConstantTimeStringComparer.Equals(payload.VerificationToken, options.VerificationToken)) + if ( + !ConstantTimeStringComparer.Equals(payload.VerificationToken, options.VerificationToken) + ) { rawJsonDocument?.Dispose(); - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(401), - IsAuthenticated = false, - IsKnownEvent = false, - Event = null, - FailureReason = "The Ko-fi verification token did not match the expected value.", - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(401), + IsAuthenticated = false, + IsKnownEvent = false, + Event = null, + FailureReason = + "The Ko-fi verification token did not match the expected value.", + } + ); } - if (!decimal.TryParse(payload.Amount, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal amount)) + if ( + !decimal.TryParse( + payload.Amount, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out decimal amount + ) + ) { rawJsonDocument?.Dispose(); - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(400), - IsAuthenticated = true, - IsKnownEvent = false, - Event = null, - FailureReason = $"The Ko-fi amount value '{payload.Amount}' could not be parsed as a decimal.", - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(400), + IsAuthenticated = true, + IsKnownEvent = false, + Event = null, + FailureReason = + $"The Ko-fi amount value '{payload.Amount}' could not be parsed as a decimal.", + } + ); } - KoFiWebhookEvent normalizedEvent = MapEvent(payload, amount, rawJsonDocument?.RootElement.Clone()); + KoFiWebhookEvent normalizedEvent = MapEvent( + payload, + amount, + rawJsonDocument?.RootElement.Clone() + ); bool isKnownEvent = normalizedEvent is not KoFiUnknownWebhookEvent; rawJsonDocument?.Dispose(); - return Task.FromResult(new WebhookHandleResult - { - Response = WebhookResponse.Empty(200), - IsAuthenticated = true, - IsKnownEvent = isKnownEvent, - Event = normalizedEvent, - FailureReason = null, - }); + return Task.FromResult( + new WebhookHandleResult + { + Response = WebhookResponse.Empty(200), + IsAuthenticated = true, + IsKnownEvent = isKnownEvent, + Event = normalizedEvent, + FailureReason = null, + } + ); } private static Dictionary ParseFormUrlEncoded(string formBody) @@ -244,15 +288,22 @@ private static string UrlDecodeFormComponent(string value) return Uri.UnescapeDataString(plusNormalized); } - private static KoFiWebhookEvent MapEvent(KoFiPayload payload, decimal amount, JsonElement? rawPayload) + private static KoFiWebhookEvent MapEvent( + KoFiPayload payload, + decimal amount, + JsonElement? rawPayload + ) { return payload.Type switch { "Donation" => CreateDonationEvent(payload, amount), - "Subscription" when payload.IsSubscriptionPayment && payload.IsFirstSubscriptionPayment => + "Subscription" + when payload.IsSubscriptionPayment && payload.IsFirstSubscriptionPayment => CreateSubscriptionStartedEvent(payload, amount), - "Subscription" when payload.IsSubscriptionPayment => - CreateSubscriptionRenewedEvent(payload, amount), + "Subscription" when payload.IsSubscriptionPayment => CreateSubscriptionRenewedEvent( + payload, + amount + ), "Shop Order" => CreateShopOrderEvent(payload, amount), "Commission" => CreateCommissionEvent(payload, amount), "Referral" => CreateReferralEvent(payload, amount), @@ -280,7 +331,10 @@ private static KoFiDonationEvent CreateDonationEvent(KoFiPayload payload, decima }; } - private static KoFiSubscriptionStartedEvent CreateSubscriptionStartedEvent(KoFiPayload payload, decimal amount) + private static KoFiSubscriptionStartedEvent CreateSubscriptionStartedEvent( + KoFiPayload payload, + decimal amount + ) { return new() { @@ -300,7 +354,10 @@ private static KoFiSubscriptionStartedEvent CreateSubscriptionStartedEvent(KoFiP }; } - private static KoFiSubscriptionRenewedEvent CreateSubscriptionRenewedEvent(KoFiPayload payload, decimal amount) + private static KoFiSubscriptionRenewedEvent CreateSubscriptionRenewedEvent( + KoFiPayload payload, + decimal amount + ) { return new() { @@ -382,7 +439,11 @@ private static KoFiCommissionEvent CreateCommissionEvent(KoFiPayload payload, de }; } - private static KoFiUnknownWebhookEvent CreateUnknownEvent(KoFiPayload payload, decimal amount, JsonElement rawPayload) + private static KoFiUnknownWebhookEvent CreateUnknownEvent( + KoFiPayload payload, + decimal amount, + JsonElement rawPayload + ) { return new() { @@ -401,4 +462,4 @@ private static KoFiUnknownWebhookEvent CreateUnknownEvent(KoFiPayload payload, d RawPayload = rawPayload, }; } -} \ No newline at end of file +} diff --git a/tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj b/tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj index e620481..aa045c4 100644 --- a/tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj +++ b/tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj @@ -1,5 +1,4 @@  - net10.0;net11.0 enable @@ -17,9 +16,6 @@ Roslyn errors asking for it rather than staying silent. CA1707/IDE1006 cover test naming, CA1515 fires only because OutputType=Exe, CA2007 is noise in test bodies. --> $(NoWarn);IDE0005;IDE1006;CA1707 - - - @@ -36,5 +32,4 @@ - diff --git a/tests/KoFi.Client.Tests/Webhooks/KoFiWebhookHandlerTests.cs b/tests/KoFi.Client.Tests/Webhooks/KoFiWebhookHandlerTests.cs index 1911d6e..f57c1ef 100644 --- a/tests/KoFi.Client.Tests/Webhooks/KoFiWebhookHandlerTests.cs +++ b/tests/KoFi.Client.Tests/Webhooks/KoFiWebhookHandlerTests.cs @@ -20,29 +20,31 @@ private static string PayloadJson( string amount = "5.00", string token = Token, bool isSubscriptionPayment = false, - bool isFirstSubscriptionPayment = false) => + bool isFirstSubscriptionPayment = false + ) => $$""" - { - "verification_token": "{{token}}", - "message_id": "msg-1", - "timestamp": "2026-01-01T12:00:00Z", - "type": "{{type}}", - "is_public": true, - "from_name": "Jane", - "message": "Thanks!", - "amount": "{{amount}}", - "currency": "USD", - "kofi_transaction_id": "txn-1", - "is_subscription_payment": {{(isSubscriptionPayment ? "true" : "false")}}, - "is_first_subscription_payment": {{(isFirstSubscriptionPayment ? "true" : "false")}} - } - """; + { + "verification_token": "{{token}}", + "message_id": "msg-1", + "timestamp": "2026-01-01T12:00:00Z", + "type": "{{type}}", + "is_public": true, + "from_name": "Jane", + "message": "Thanks!", + "amount": "{{amount}}", + "currency": "USD", + "kofi_transaction_id": "txn-1", + "is_subscription_payment": {{(isSubscriptionPayment ? "true" : "false")}}, + "is_first_subscription_payment": {{(isFirstSubscriptionPayment ? "true" : "false")}} + } + """; private static WebhookRequest Request( string? json = null, string method = "POST", string contentType = "application/x-www-form-urlencoded", - string? rawBody = null) + string? rawBody = null + ) { string body = rawBody ?? $"data={Uri.EscapeDataString(json ?? PayloadJson())}"; @@ -58,8 +60,10 @@ private static WebhookRequest Request( [TestMethod] public async Task HandleAsync_WhenMethodIsNotPost_Returns405() { - WebhookHandleResult result = - await _handler.HandleAsync(Request(method: "GET"), Options); + WebhookHandleResult result = await _handler.HandleAsync( + Request(method: "GET"), + Options + ); Assert.AreEqual(405, result.Response.StatusCode); Assert.IsFalse(result.IsAuthenticated); @@ -69,8 +73,10 @@ public async Task HandleAsync_WhenMethodIsNotPost_Returns405() [TestMethod] public async Task HandleAsync_WhenContentTypeIsNotFormUrlEncoded_Returns400() { - WebhookHandleResult result = - await _handler.HandleAsync(Request(contentType: "application/json"), Options); + WebhookHandleResult result = await _handler.HandleAsync( + Request(contentType: "application/json"), + Options + ); Assert.AreEqual(400, result.Response.StatusCode); Assert.IsFalse(result.IsAuthenticated); @@ -79,8 +85,10 @@ public async Task HandleAsync_WhenContentTypeIsNotFormUrlEncoded_Returns400() [TestMethod] public async Task HandleAsync_WhenDataFieldIsMissing_Returns400() { - WebhookHandleResult result = - await _handler.HandleAsync(Request(rawBody: "other=value"), Options); + WebhookHandleResult result = await _handler.HandleAsync( + Request(rawBody: "other=value"), + Options + ); Assert.AreEqual(400, result.Response.StatusCode); Assert.IsNotNull(result.FailureReason); @@ -90,8 +98,10 @@ public async Task HandleAsync_WhenDataFieldIsMissing_Returns400() [TestMethod] public async Task HandleAsync_WhenDataIsNotValidJson_Returns400() { - WebhookHandleResult result = - await _handler.HandleAsync(Request(rawBody: "data=not-json"), Options); + WebhookHandleResult result = await _handler.HandleAsync( + Request(rawBody: "data=not-json"), + Options + ); Assert.AreEqual(400, result.Response.StatusCode); Assert.IsFalse(result.IsAuthenticated); @@ -100,8 +110,10 @@ public async Task HandleAsync_WhenDataIsNotValidJson_Returns400() [TestMethod] public async Task HandleAsync_WhenVerificationTokenDoesNotMatch_Returns401() { - WebhookHandleResult result = - await _handler.HandleAsync(Request(PayloadJson(token: "wrong-token")), Options); + WebhookHandleResult result = await _handler.HandleAsync( + Request(PayloadJson(token: "wrong-token")), + Options + ); Assert.AreEqual(401, result.Response.StatusCode); Assert.IsFalse(result.IsAuthenticated); @@ -111,8 +123,10 @@ public async Task HandleAsync_WhenVerificationTokenDoesNotMatch_Returns401() [TestMethod] public async Task HandleAsync_WhenAmountIsNotParseable_Returns400ButIsAuthenticated() { - WebhookHandleResult result = - await _handler.HandleAsync(Request(PayloadJson(amount: "not-a-number")), Options); + WebhookHandleResult result = await _handler.HandleAsync( + Request(PayloadJson(amount: "not-a-number")), + Options + ); Assert.AreEqual(400, result.Response.StatusCode); Assert.IsTrue(result.IsAuthenticated); @@ -122,8 +136,10 @@ public async Task HandleAsync_WhenAmountIsNotParseable_Returns400ButIsAuthentica [TestMethod] public async Task HandleAsync_WhenPayloadIsAValidDonation_ReturnsDonationEvent() { - WebhookHandleResult result = - await _handler.HandleAsync(Request(), Options); + WebhookHandleResult result = await _handler.HandleAsync( + Request(), + Options + ); Assert.AreEqual(200, result.Response.StatusCode); Assert.IsTrue(result.IsAuthenticated); @@ -141,8 +157,15 @@ public async Task HandleAsync_WhenPayloadIsAValidDonation_ReturnsDonationEvent() public async Task HandleAsync_WhenFirstSubscriptionPayment_ReturnsSubscriptionStarted() { WebhookHandleResult result = await _handler.HandleAsync( - Request(PayloadJson("Subscription", isSubscriptionPayment: true, isFirstSubscriptionPayment: true)), - Options); + Request( + PayloadJson( + "Subscription", + isSubscriptionPayment: true, + isFirstSubscriptionPayment: true + ) + ), + Options + ); _ = Assert.IsInstanceOfType(result.Event); } @@ -152,7 +175,8 @@ public async Task HandleAsync_WhenRecurringSubscriptionPayment_ReturnsSubscripti { WebhookHandleResult result = await _handler.HandleAsync( Request(PayloadJson("Subscription", isSubscriptionPayment: true)), - Options); + Options + ); _ = Assert.IsInstanceOfType(result.Event); } @@ -163,8 +187,10 @@ public async Task HandleAsync_WhenRecurringSubscriptionPayment_ReturnsSubscripti [DataRow("Referral", typeof(KoFiReferralEvent))] public async Task HandleAsync_MapsKnownPayloadTypes(string payloadType, Type expected) { - WebhookHandleResult result = - await _handler.HandleAsync(Request(PayloadJson(payloadType)), Options); + WebhookHandleResult result = await _handler.HandleAsync( + Request(PayloadJson(payloadType)), + Options + ); Assert.IsTrue(result.IsKnownEvent); Assert.IsInstanceOfType(result.Event, expected); @@ -173,8 +199,10 @@ public async Task HandleAsync_MapsKnownPayloadTypes(string payloadType, Type exp [TestMethod] public async Task HandleAsync_WhenPayloadTypeIsUnrecognised_ReturnsUnknownEventStill200() { - WebhookHandleResult result = - await _handler.HandleAsync(Request(PayloadJson("Something New")), Options); + WebhookHandleResult result = await _handler.HandleAsync( + Request(PayloadJson("Something New")), + Options + ); Assert.AreEqual(200, result.Response.StatusCode); Assert.IsTrue(result.IsAuthenticated); @@ -188,17 +216,20 @@ public async Task HandleAsync_WhenCancelled_Throws() using CancellationTokenSource cts = new(); await cts.CancelAsync(); - _ = await Assert.ThrowsExactlyAsync( - () => _handler.HandleAsync(Request(), Options, cts.Token)); + _ = await Assert.ThrowsExactlyAsync(() => + _handler.HandleAsync(Request(), Options, cts.Token) + ); } [TestMethod] - public async Task HandleAsync_WhenRequestIsNull_Throws() - => await Assert.ThrowsExactlyAsync( - () => _handler.HandleAsync(null!, Options)); + public async Task HandleAsync_WhenRequestIsNull_Throws() => + await Assert.ThrowsExactlyAsync(() => + _handler.HandleAsync(null!, Options) + ); [TestMethod] - public async Task HandleAsync_WhenOptionsAreNull_Throws() - => await Assert.ThrowsExactlyAsync( - () => _handler.HandleAsync(Request(), null!)); + public async Task HandleAsync_WhenOptionsAreNull_Throws() => + await Assert.ThrowsExactlyAsync(() => + _handler.HandleAsync(Request(), null!) + ); } From 16fb76cdbeb8ce5c2425a87286357814456b5425 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 23 Sep 2026 11:05:30 +0200 Subject: [PATCH 2/3] build: move to the .NET 11 RC1 SDK --- .github/workflows/build.yml | 2 +- global.json | 2 +- samples/KoFi.Client.Sample/KoFi.Client.Sample.csproj | 6 ++---- tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj | 9 +++------ 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ee11ff..9289ded 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: with: dotnet-version: | 10.0.x - 11.0.100-preview.7.26381.103 + 11.0.100-rc.1.26425.128 - name: Restore dependencies run: dotnet restore KoFi.Client.slnx diff --git a/global.json b/global.json index 6827f37..0f28551 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "11.0.100-preview.7.26381.103", + "version": "11.0.100-rc.1.26425.128", "allowPrerelease": true, "rollForward": "latestPreview" }, diff --git a/samples/KoFi.Client.Sample/KoFi.Client.Sample.csproj b/samples/KoFi.Client.Sample/KoFi.Client.Sample.csproj index 8205418..d604d66 100644 --- a/samples/KoFi.Client.Sample/KoFi.Client.Sample.csproj +++ b/samples/KoFi.Client.Sample/KoFi.Client.Sample.csproj @@ -1,15 +1,13 @@  - net10.0 + net11.0 Exe enable enable preview true false - false - false - $(NoWarn);CA1305 + true KoFi.Client.Sample diff --git a/tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj b/tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj index aa045c4..4bfbfc9 100644 --- a/tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj +++ b/tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj @@ -8,14 +8,11 @@ Exe true - false + true - - - - $(NoWarn);IDE0005;IDE1006;CA1707 + $(NoWarn);IDE1006;CA1707;CS1591 From 002d835133ed292e46e619e4bb302909d11439b7 Mon Sep 17 00:00:00 2001 From: Agash Date: Wed, 23 Sep 2026 11:27:06 +0200 Subject: [PATCH 3/3] ci: pass test targets to dotnet test explicitly --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9289ded..fb2ba00 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: run: dotnet build KoFi.Client.slnx --configuration Release --no-restore - name: Run unit tests - run: dotnet test tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj --configuration Release --no-build --verbosity normal -- --coverage --coverage-settings coverage.settings.xml --coverage-output-format cobertura --coverage-output coverage.cobertura.xml + run: dotnet test --project ${{ github.workspace }}/tests/KoFi.Client.Tests/KoFi.Client.Tests.csproj --configuration Release --no-build --verbosity normal -- --coverage --coverage-settings coverage.settings.xml --coverage-output-format cobertura --coverage-output coverage.cobertura.xml - name: Summarize coverage