diff --git a/Directory.Build.props b/Directory.Build.props index f2220d0..ba67a2b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,11 +1,6 @@ - $(MSBuildThisFileDirectory) - $(RepositoryDirectory)build\ - - - - 1.0.1 + 1.0.2 1.0.0 @@ -41,6 +36,11 @@ $(NoWarn);CS8500 + + $(MSBuildThisFileDirectory) + $(RepositoryDirectory)build\ + + $(MSBuildProjectName.Contains('Test')) False diff --git a/Directory.Build.targets b/Directory.Build.targets index af26e61..f42bc22 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -4,7 +4,7 @@ - $(CommonTags);.NET + $(CommonTags);fluent;.NET $(CommonTags);$(PackageTags) $(CommonTags) diff --git a/README.md b/README.md index e76dcd9..8709c81 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,141 @@ -# Fluent Framework +
+

๐Ÿš€ Fluent Framework

+

A modern collection of libraries for .NET applications.

+
-[Created with love in Poland by Leszek Pomianowski](https://lepo.co/) and [wonderful open-source community](https://github.com/lepoco/fluent/graphs/contributors). -Fluent is an advanced .NET framework for UI and application development. +

+ Build better .NET applications with fluent, expressive APIs across HTTP, UI, and more. +

+ +

+ GitHub stars + License + Contributors +

+ +

+ Created with โค๏ธ in Poland by Leszek Pomianowski and open-source community. +

+ +--- + +## ๐Ÿ“ฆ Packages + +Fluent Framework is a collection of independent packages. Use what you need. + +| Package | Description | NuGet | +|---------|-------------|-------| +| [**Fluent.Client**](src/Fluent.Client) | Fluent HTTP client wrapper for clean, chainable requests | [![NuGet](https://img.shields.io/nuget/v/Fluent.Client.svg)](https://www.nuget.org/packages/Fluent.Client) | +| [**Fluent.Client.AwesomeAssertions**](src/Fluent.Client.AwesomeAssertions) | Expressive HTTP response assertions for integration tests | [![NuGet](https://img.shields.io/nuget/v/Fluent.Client.AwesomeAssertions.svg)](https://www.nuget.org/packages/Fluent.Client.AwesomeAssertions) | + +> [!NOTE] +> More packages coming soon. Stay tuned! + +--- + +## โšก Quick Start + +### HTTP Client + +```powershell +dotnet add package Fluent.Client +``` + +```csharp +using Fluent.Client; + +var client = new HttpClient { BaseAddress = new Uri("https://api.example.com/") }; + +// Clean, fluent HTTP requests +var response = await client + .Authorize(token: "jwt-token") + .Post("/api/users", new { Name = "John" }); +``` + +### Testing Assertions + +```powershell +dotnet add package Fluent.Client.AwesomeAssertions +``` + +```csharp +using Fluent.Client.AwesomeAssertions; + +// Expressive test assertions +await client + .Post("/api/users", new { Name = "John" }) + .Should() + .Succeed("because valid data was provided"); +``` + +--- + +## ๐ŸŽฏ Philosophy + +Fluent Framework follows these principles: + +| Principle | Description | +|-----------|-------------| +| **๐Ÿงฉ Modular** | Use only what you need. Each package is independent. | +| **๐Ÿ“– Readable** | APIs designed to read like natural language. | +| **๐Ÿ”ง Extensible** | Easy to extend and customize for your needs. | +| **โœ… Testable** | Built with testing in mind from the ground up. | +| **๐Ÿš€ Modern** | Targets latest .NET with modern C# features. | + +--- + +## ๐Ÿ› ๏ธ Building from Source + +### Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download) or later +- Visual Studio 2022, VS Code, or JetBrains Rider + +### Build + +```powershell +git clone https://github.com/lepoco/fluent.git +cd fluent +dotnet build +``` + +### Run Tests + +```powershell +dotnet test +``` + +--- + +## ๐Ÿค Contributing + +We welcome contributions! Please see our [Contributing Guide](Contributing.md) for details. + +### Ways to Contribute + +- ๐Ÿ› Report bugs and issues +- ๐Ÿ’ก Suggest new features or packages +- ๐Ÿ“ Improve documentation +- ๐Ÿ”ง Submit pull requests + +--- + +## ๐Ÿ‘ฅ Maintainers + +- Leszek Pomianowski ([@pomianowski](https://github.com/pomianowski)) + +--- + +## ๐Ÿ’ฌ Support + +- ๐Ÿ“– [Documentation](docs/) +- ๐Ÿ› [Issue Tracker](https://github.com/lepoco/fluent/issues) +- ๐Ÿ’ฌ [Discussions](https://github.com/lepoco/fluent/discussions) + +--- + +## ๐Ÿ“„ License + +This project is licensed under the **MIT** license. See the [LICENSE](LICENSE) file for details. + +You can use it in private and commercial projects. Keep in mind that you must include a copy of the license in your project. diff --git a/build/nuget.png b/build/nuget.png index 7c66e73..818e367 100644 Binary files a/build/nuget.png and b/build/nuget.png differ diff --git a/src/Fluent.Client.AwesomeAssertions/Fluent.Client.AwesomeAssertions.csproj b/src/Fluent.Client.AwesomeAssertions/Fluent.Client.AwesomeAssertions.csproj index 7a6ef34..ba90b0c 100644 --- a/src/Fluent.Client.AwesomeAssertions/Fluent.Client.AwesomeAssertions.csproj +++ b/src/Fluent.Client.AwesomeAssertions/Fluent.Client.AwesomeAssertions.csproj @@ -2,6 +2,7 @@ net10.0;net8.0;net472;net481 true + assertions;fluentassertions;awesome;awesomeassertions;testing;xunit;nunit;playwright;http;client diff --git a/src/Fluent.Client.AwesomeAssertions/HttpResponseMessageTaskAssertions.cs b/src/Fluent.Client.AwesomeAssertions/HttpResponseMessageTaskAssertions.cs index b2aa3c6..bfa00d9 100644 --- a/src/Fluent.Client.AwesomeAssertions/HttpResponseMessageTaskAssertions.cs +++ b/src/Fluent.Client.AwesomeAssertions/HttpResponseMessageTaskAssertions.cs @@ -14,10 +14,12 @@ namespace Fluent.Client.AwesomeAssertions; public class HttpResponseMessageTaskAssertions(Task instance, AssertionChain chain) : ReferenceTypeAssertions, HttpResponseMessageTaskAssertions>(instance, chain) { - private AssertionChain chain = chain; + private readonly AssertionChain chain = chain; protected override string Identifier => "http-response"; + // ReSharper disable once MemberCanBePrivate.Global + // ReSharper disable once FieldCanBeMadeReadOnly.Global public static JsonSerializerOptions DefaultJsonOptions = new() { PropertyNameCaseInsensitive = true, @@ -128,7 +130,23 @@ params object[] becauseArgs using (AssertionScope assertionScope = new()) { - TBody? body = JsonSerializer.Deserialize(rawResponse, DefaultJsonOptions); + TBody? body; + + try + { + body = JsonSerializer.Deserialize(rawResponse, DefaultJsonOptions); + } + catch (Exception e) + { + CurrentAssertionChain + .WithDefaultIdentifier(Identifier) + .WithExpectation( + $"Expected HTTP response content to be deserializable to \"{typeof(TBody).Name}\", but deserialization threw an exception:", + assertionChain => assertionChain.FailWith(e.ToString()) + ); + + return; + } assertion(body!); failuresFromInspector = assertionScope.Discard(); diff --git a/src/Fluent.Client.AwesomeAssertions/HttpResponseMessageTaskExtensions.cs b/src/Fluent.Client.AwesomeAssertions/HttpResponseMessageTaskExtensions.cs index 5159f87..ff1ab6f 100644 --- a/src/Fluent.Client.AwesomeAssertions/HttpResponseMessageTaskExtensions.cs +++ b/src/Fluent.Client.AwesomeAssertions/HttpResponseMessageTaskExtensions.cs @@ -12,7 +12,7 @@ public static class HttpResponseMessageTaskExtensions extension(Task response) { /// - /// Returns assertions for . + /// Returns assertions for . /// public HttpResponseMessageTaskAssertions Should() => new(response, AssertionChain.GetOrCreate()); } diff --git a/src/Fluent.Client.AwesomeAssertions/README.md b/src/Fluent.Client.AwesomeAssertions/README.md index 019886b..648fd7a 100644 --- a/src/Fluent.Client.AwesomeAssertions/README.md +++ b/src/Fluent.Client.AwesomeAssertions/README.md @@ -1,89 +1,385 @@ -# Fluent HttpClient AwesomeAssertions for testing .NET apps +
+

๐Ÿงช Fluent.Client.AwesomeAssertions

+

Write expressive HTTP assertions for .NET integration tests.

+
-[Created in Poland by Leszek Pomianowski](https://lepo.co/) and [open-source community](https://github.com/lepoco/fluent/graphs/contributors). -Fluent HttpClient AwesomeAssertions provides a set of fluent assertions for `Task`. +

+ A fluent assertion library for Task<HttpResponseMessage> that makes your HTTP tests readable, maintainable, and delightful to write. +

-> **Note** -> `Fluent.Client` is optional. You can use this library with standard `HttpClient`. +

+ NuGet + NuGet Downloads + GitHub stars + License +

-## Getting started +

+ Created in Poland by Leszek Pomianowski and open-source community. +

-You can add it to your project using .NET CLI: +--- + +## Table of Contents + +- [Table of Contents](#table-of-contents) +- [๐Ÿค” Why This Library?](#-why-this-library) +- [โšก Get Started](#-get-started) + - [1. Install the Package](#1-install-the-package) + - [2. (Optional) Add Fluent.Client](#2-optional-add-fluentclient) + - [3. Start Writing Tests](#3-start-writing-tests) +- [๐Ÿ“– How to Use](#-how-to-use) + - [1. Assert Success](#1-assert-success) + - [2. Assert Specific Status Code](#2-assert-specific-status-code) + - [Quick Reference](#quick-reference) + - [3. Assert Failure](#3-assert-failure) + - [4. Assert Body Content](#4-assert-body-content) + - [5. Authorization with Query Parameters](#5-authorization-with-query-parameters) + - [Bearer Token Authorization](#bearer-token-authorization) + - [Basic Authentication](#basic-authentication) + - [Authorization Methods](#authorization-methods) +- [๐Ÿงช Integration Testing](#-integration-testing) + - [Key Patterns](#key-patterns) +- [๐Ÿ“š API Reference](#-api-reference) + - [Assertion Methods](#assertion-methods) + - [JSON Serialization](#json-serialization) +- [๐Ÿ‘ฅ Maintainers](#-maintainers) +- [๐Ÿ’ฌ Support](#-support) +- [๐Ÿ™ Acknowledgements](#-acknowledgements) +- [๐Ÿ“„ License](#-license) + +--- + +## ๐Ÿค” Why This Library? + +Traditional HTTP testing in .NET is verbose and hard to read: + +```csharp +// โŒ Traditional approach - verbose and unclear intent +var response = await client.PostAsync("/api/users", content); +Assert.True(response.IsSuccessStatusCode); +var body = await response.Content.ReadAsStringAsync(); +var user = JsonSerializer.Deserialize(body); +Assert.Equal("John", user.Name); +``` + +With **Fluent.Client.AwesomeAssertions**, your tests become expressive and self-documenting: + +```csharp +// โœ… Fluent approach - clear intent, readable assertions +await client + .Post("/api/users", new { Name = "John" }) + .Should() + .Satisfy(u => u.Name.Should().Be("John")); +``` + +> [!NOTE] +> `Fluent.Client` is optional. This library works with standard `HttpClient.PostAsync()`, `GetAsync()`, etc. + +--- + +## โšก Get Started + +### 1. Install the Package ```powershell dotnet add package Fluent.Client.AwesomeAssertions ``` -## How to use +๐Ÿ“ฆ **NuGet:** + +### 2. (Optional) Add Fluent.Client + +For an even more fluent API experience: + +```powershell +dotnet add package Fluent.Client +``` + +๐Ÿ“ฆ **NuGet:** -### 1. Assert success +> [!TIP] +> With `Fluent.Client`, you get extension methods like `.Post()`, `.Get()`, `.Delete()`, and `.Authorize()` directly on `HttpClient`. -You can assert that a request was successful (2xx status code). +### 3. Start Writing Tests -**Standard HttpClient** +```csharp +using Fluent.Client; +using Fluent.Client.AwesomeAssertions; + +[Fact] +public async Task CreateUser_ReturnsSuccess() +{ + await client + .Post("/api/users", new { Name = "John" }) + .Should() + .Succeed("because valid user data was provided"); +} +``` + +--- + +## ๐Ÿ“– How to Use + +### 1. Assert Success + +Assert that a request was successful (2xx status code). + +
+Standard HttpClient ```csharp await client .PostAsync("/api/users", content) - .Should().Succeed(); + .Should() + .Succeed(); ``` -**Fluent.Client** +
+ +
+With Fluent.Client ```csharp await client .Post("/api/users", new { Name = "John" }) - .Should().Succeed(); + .Should() + .Succeed("because the server returned 200 OK"); ``` -### 2. Assert specific status code +
+ +> [!TIP] +> The `because` parameter is optional but recommended for clearer test failure messages. -You can assert that a request returned a specific status code. +--- +### 2. Assert Specific Status Code -**With Fluent.Client** +Assert that a request returned a specific HTTP status code. ```csharp await client .Delete("/api/users/123") - .Should().HaveStatusCode(HttpStatusCode.NoContent); + .Should() + .HaveStatusCode(HttpStatusCode.NoContent, "because delete should return 204"); ``` -### 3. Assert failure +#### Quick Reference -You can assert that a request failed (non-2xx status code). +| Method | Description | +|--------|-------------| +| `HaveStatusCode(HttpStatusCode)` | Asserts exact status code match | +| `Succeed()` | Asserts any 2xx status code | +| `Fail()` | Asserts any non-2xx status code | -**With Fluent.Client** +--- + +### 3. Assert Failure + +Assert that a request failed (non-2xx status code). ```csharp await client - .Get("/api/unknown") - .Should().Fail(); + .Post("/api/basket", new { CartItem = "esp32-dev-board" }) + .Should() + .Fail("because the server returned 400 Bad Request"); ``` -### 4. Assert body content +> [!IMPORTANT] +> `Fail()` passes for **any** non-success status code (4xx, 5xx). Use `HaveStatusCode()` if you need to verify a specific error code. -You can assert on the deserialized body content. +--- -**With Fluent.Client** +### 4. Assert Body Content + +Assert on the deserialized response body using `Satisfy`. ```csharp await client .Authorize(token: "abc123") - .Get("/api/users/1") + .Get("/api/users/1", new + { + // Query parameters as anonymous object + includeDetails = true + }) .Should() .Satisfy(user => { user.Name.Should().Be("John"); - user.Id.Should().Be("1"); - }); + user.Id.Should().Be(1); + }, "because the server returned the expected JSON body"); +``` + +
+How it works + +1. Awaits the HTTP response +2. Reads the response body as string +3. Deserializes JSON to `T` using `System.Text.Json` +4. Executes your assertion lambda against the deserialized object + +
+ +> [!WARNING] +> If the response body is not valid JSON or cannot be deserialized to `T`, the assertion will fail with a descriptive error message. + +--- + +### 5. Authorization with Query Parameters + +Combine authorization headers with query parameters for authenticated requests. + +#### Bearer Token Authorization + +```csharp +await client + .Authorize(token: "abc123") + .Post("/v1/api/basket") + .Should() + .Succeed(); +``` + +#### Basic Authentication + +```csharp +await client + .Authorize(username: "john", password: "potato") + .Get("/v1/api/basket", new + { + page = 1, + limit = 2, + sortBy = "dateAsc", + }) + .Should() + .HaveStatusCode(HttpStatusCode.Unauthorized, "because the credentials are invalid"); +``` + +#### Authorization Methods + +| Method | Header Format | +|--------|---------------| +| `.Authorize(token: "...")` | `Authorization: Bearer {token}` | +| `.Authorize(username, password)` | `Authorization: Basic {base64}` | + +--- + +## ๐Ÿงช Integration Testing + +The library excels in integration testing scenarios with multi-step workflows. + +
+Complete Workflow Example + +```csharp +[Collection("Integration Tests")] +public sealed class OrderWorkflowTests(AspireAppHostFixture app) +{ + [Fact] + public async Task Order_WhenCreatedAndProcessed_CompletesSuccessfully() + { + Guid orderId = Guid.NewGuid(); + + // Step 1: Create resource + await app.Client + .Authorize(token: "jwt-token") + .Put($"v1/orders/{orderId}", new { ProductId = "SKU-001", Quantity = 2 }) + .Should() + .Succeed("because order creation should be accepted"); + + // Step 2: Verify resource state + await app.Client + .Authorize(token: "jwt-token") + .Get($"v1/orders/{orderId}") + .Should() + .Satisfy(order => + { + order.Status.Should().Be("Pending"); + order.Id.Should().Be(orderId); + }); + + // Step 3: Transition state + await app.Client + .Authorize(token: "jwt-token") + .Put($"v1/orders/{orderId}/confirm") + .Should() + .Succeed("because order confirmation should succeed"); + + // Step 4: Complete workflow + await app.Client + .Authorize(token: "jwt-token") + .Put($"v1/orders/{orderId}/complete", new { Note = "Delivered" }) + .Should() + .Succeed("because order completion should succeed"); + } +} ``` -## Code of Conduct +
+ +### Key Patterns + +| Pattern | Description | +|---------|-------------| +| ๐Ÿ—๏ธ **Test Fixtures** | Use `WebApplicationFactory`, `AspireAppHostFixture`, or similar for shared test setup | +| ๐Ÿ”— **Workflow Chaining** | Chain multiple API calls to test complete business flows | +| โœ… **State Verification** | Use `Satisfy` to verify intermediate states | +| ๐Ÿ“ **Descriptive Messages** | Add `because` messages for clear failure diagnostics | + +--- + +## ๐Ÿ“š API Reference + +### Assertion Methods + +| Method | Description | +|--------|-------------| +| `Succeed()` | Asserts HTTP response has 2xx status code | +| `Succeed(HttpStatusCode)` | Asserts HTTP response has specific success status code | +| `Fail()` | Asserts HTTP response has non-2xx status code | +| `HaveStatusCode(HttpStatusCode)` | Asserts HTTP response has exact status code | +| `Satisfy(Action)` | Deserializes body and runs assertions on the result | + +### JSON Serialization + +
+Satisfy<T> uses the following JsonSerializerOptions by default + +```csharp +new JsonSerializerOptions +{ + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + WriteIndented = true, + IncludeFields = false, + Converters = { new JsonStringEnumConverter() } +} +``` + +
+ +--- + +## ๐Ÿ‘ฅ Maintainers + +- Leszek Pomianowski ([@pomianowski](https://github.com/pomianowski)) + +--- + +## ๐Ÿ’ฌ Support + +For support, please open a [GitHub issue](https://github.com/lepoco/fluent/issues/new). We welcome bug reports, feature requests, and questions. + +--- + +## ๐Ÿ™ Acknowledgements + +This project builds on the excellent [AwesomeAssertions](https://github.com/awesomeassertions/awesomeassertions) library and is inspired by the need for better HTTP testing ergonomics in .NET. + +--- -This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. +## ๐Ÿ“„ License -## License +This project is licensed under the terms of the **MIT** open source license. Please refer to the [LICENSE](../../LICENSE) file for the full terms. -Fluent HttpClient AwesomeAssertions is free and open source software licensed under MIT License. You can use it in private and commercial projects. -Keep in mind that you must include a copy of the license in your project. +You can use it in private and commercial projects. Keep in mind that you must include a copy of the license in your project. diff --git a/src/Fluent.Client/Fluent.Client.csproj b/src/Fluent.Client/Fluent.Client.csproj index 60c1de1..a73d2bc 100644 --- a/src/Fluent.Client/Fluent.Client.csproj +++ b/src/Fluent.Client/Fluent.Client.csproj @@ -2,6 +2,7 @@ net10.0;net8.0;net472;net481 true + http;client diff --git a/src/Fluent.Client/README.md b/src/Fluent.Client/README.md index 26fbeae..7543e6f 100644 --- a/src/Fluent.Client/README.md +++ b/src/Fluent.Client/README.md @@ -1,56 +1,278 @@ -# Fluent Client for .NET HttpClient. +
+

๐ŸŒŠ Fluent.Client

+

A fluent HTTP client wrapper for .NET.

+
-[Created in Poland by Leszek Pomianowski](https://lepo.co/) and [open-source community](https://github.com/lepoco/fluent/graphs/contributors). -Fluent Client provides a way to build HTTP requests. It acts as a wrapper around the standard HttpClient, allowing you to set up your requests with a body, headers, queries, and other parameters before sending them. +

+ Build HTTP requests with a clean, chainable API. Less boilerplate, more productivity. +

-## Getting started +

+ NuGet + NuGet Downloads + GitHub stars + License +

-You can add it to your project using .NET CLI: +

+ Created in Poland by Leszek Pomianowski and open-source community. +

+ +--- + +## Table of Contents + +- [Table of Contents](#table-of-contents) +- [๐Ÿค” Why This Library?](#-why-this-library) +- [โšก Get Started](#-get-started) + - [Install the Package](#install-the-package) + - [Quick Example](#quick-example) +- [๐Ÿ“– How to Use](#-how-to-use) + - [1. Create a Request](#1-create-a-request) + - [Available HTTP Methods](#available-http-methods) + - [2. Configure the Request](#2-configure-the-request) + - [Authorization](#authorization) + - [Query Parameters](#query-parameters) + - [Chaining Multiple Configurations](#chaining-multiple-configurations) + - [3. Send the Request](#3-send-the-request) +- [๐Ÿ“š API Reference](#-api-reference) + - [Request Creation Methods](#request-creation-methods) + - [Configuration Methods](#configuration-methods) + - [Execution Methods](#execution-methods) +- [๐Ÿงช Testing with AwesomeAssertions](#-testing-with-awesomeassertions) +- [๐Ÿ‘ฅ Maintainers](#-maintainers) +- [๐Ÿ’ฌ Support](#-support) +- [๐Ÿ“„ License](#-license) + +--- + +## ๐Ÿค” Why This Library? + +Traditional `HttpClient` usage requires verbose setup: + +```csharp +// โŒ Traditional approach - verbose and repetitive +var request = new HttpRequestMessage(HttpMethod.Post, "/api/users"); +request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); +request.Content = new StringContent( + JsonSerializer.Serialize(new { Name = "John" }), + Encoding.UTF8, + "application/json"); +var response = await client.SendAsync(request); +``` + +With **Fluent.Client**, requests become expressive one-liners: + +```csharp +// โœ… Fluent approach - clean and readable +var response = await client + .Authorize(token: "abc123") + .Post("/api/users", new { Name = "John" }) + .SendAsync(); +``` + +--- + +## โšก Get Started + +### Install the Package ```powershell dotnet add package Fluent.Client ``` -## How to use +๐Ÿ“ฆ **NuGet:** + +### Quick Example + +```csharp +using Fluent.Client; + +var client = new HttpClient { BaseAddress = new Uri("https://api.example.com/") }; + +// Simple POST with JSON body +var response = await client + .Post("/api/users", new { Name = "John Doe" }) + .SendAsync(); +``` + +--- + +## ๐Ÿ“– How to Use -### 1. Create a request +### 1. Create a Request -You can start by creating a request with a body using the `With` method on your HttpClient. +Start by creating a request using one of the HTTP method extensions on `HttpClient`. ```csharp -using Fluent.HttpClient; +using Fluent.Client; -var client = new HttpClient(); -client.BaseAddress = new Uri("https://api.example.com/"); +var client = new HttpClient { BaseAddress = new Uri("https://api.example.com/") }; +// POST with JSON body var request = client.Post("/api/v1/users", new { Name = "John Doe" }); + +// GET with query parameters +var request = client.Get("/api/v1/users", new { page = 1, limit = 10 }); + +// DELETE +var request = client.Delete("/api/v1/users/897"); + +// PUT with body +var request = client.Put("/api/v1/users/897", new { Name = "Jane Doe" }); ``` -### 2. Configure the request +#### Available HTTP Methods + +| Method | Description | +|--------|-------------| +| `.Get(path, query?)` | Create GET request with optional query parameters | +| `.Post(path, body?)` | Create POST request with optional JSON body | +| `.Put(path, body?)` | Create PUT request with optional JSON body | +| `.Delete(path)` | Create DELETE request | +| `.Patch(path, body?)` | Create PATCH request with optional JSON body | + +--- -You can configure its properties like the path, HTTP method, and headers. +### 2. Configure the Request + +Chain configuration methods to add headers, authorization, and more. + +#### Authorization ```csharp -client.Authorize(token: "123").Delete("/api/v1/users/897"); +// Bearer token +client.Authorize(token: "jwt-token-here").Get("/api/protected"); + +// Basic authentication +client.Authorize(username: "john", password: "secret").Get("/api/protected"); ``` -### 3. Send the request +#### Query Parameters -You can send the request and get the response message, or automatically deserialize the response content. +```csharp +// Pass as anonymous object +client.Get("/api/users", new +{ + page = 1, + limit = 10, + sortBy = "createdAt" +}); +``` + +#### Chaining Multiple Configurations + +```csharp +var request = client + .Authorize(token: "abc123") + .Get("/api/v1/basket", new { includeItems = true }); +``` + +--- + +### 3. Send the Request + +Send the request and handle the response. + +
+Get HttpResponseMessage ```csharp -// Send and get the HttpResponseMessage using HttpResponseMessage response = await request.SendAsync(); -// or send and deserialize the response +if (response.IsSuccessStatusCode) +{ + var content = await response.Content.ReadAsStringAsync(); + Console.WriteLine(content); +} +``` + +
+ +
+Deserialize Response Automatically + +```csharp +// Automatically deserialize JSON response to typed object UserCreatedResponse result = await request.SendAsync(); + +Console.WriteLine($"Created user: {result.Id}"); ``` -## Code of Conduct +
+ +
+Direct Execution (No SendAsync) + +```csharp +// The request returns Task, so you can await directly +using var response = await client + .Authorize(token: "abc123") + .Post("/api/users", new { Name = "John" }); +``` + +
+ +--- + +## ๐Ÿ“š API Reference + +### Request Creation Methods + +| Method | Description | +|--------|-------------| +| `Get(path, query?)` | Create GET request | +| `Post(path, body?)` | Create POST request | +| `Put(path, body?)` | Create PUT request | +| `Delete(path)` | Create DELETE request | +| `Patch(path, body?)` | Create PATCH request | + +### Configuration Methods + +| Method | Description | +|--------|-------------| +| `Authorize(token)` | Add Bearer token authorization | +| `Authorize(username, password)` | Add Basic authentication | + +### Execution Methods + +| Method | Description | +|--------|-------------| +| `SendAsync()` | Send request and return `HttpResponseMessage` | +| `SendAsync()` | Send request and deserialize response to `T` | + +--- + +## ๐Ÿงช Testing with AwesomeAssertions + +Pair this library with [Fluent.Client.AwesomeAssertions](https://www.nuget.org/packages/Fluent.Client.AwesomeAssertions) for expressive test assertions: + +```csharp +await client + .Authorize(token: "abc123") + .Post("/api/users", new { Name = "John" }) + .Should() + .Succeed("because valid user data was provided"); +``` + +๐Ÿ“ฆ **Install:** `dotnet add package Fluent.Client.AwesomeAssertions` + +--- + +## ๐Ÿ‘ฅ Maintainers + +- Leszek Pomianowski ([@pomianowski](https://github.com/pomianowski)) + +--- + +## ๐Ÿ’ฌ Support + +For support, please open a [GitHub issue](https://github.com/lepoco/fluent/issues/new). We welcome bug reports, feature requests, and questions. + +--- -This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. +## ๐Ÿ“„ License -## License +This project is licensed under the terms of the **MIT** open source license. Please refer to the [LICENSE](../../LICENSE) file for the full terms. -Fluent HttpClient is free and open source software licensed under MIT License. You can use it in private and commercial projects. -Keep in mind that you must include a copy of the license in your project. +You can use it in private and commercial projects. Keep in mind that you must include a copy of the license in your project. diff --git a/tests/Fluent.Client.AwesomeAssertions.UnitTests/HttpResponseMessageTaskAssertionsTests.cs b/tests/Fluent.Client.AwesomeAssertions.UnitTests/HttpResponseMessageTaskAssertionsTests.cs index 38be3c1..86924ce 100644 --- a/tests/Fluent.Client.AwesomeAssertions.UnitTests/HttpResponseMessageTaskAssertionsTests.cs +++ b/tests/Fluent.Client.AwesomeAssertions.UnitTests/HttpResponseMessageTaskAssertionsTests.cs @@ -1,4 +1,4 @@ -๏ปฟ// This Source Code Form is subject to the terms of the MIT License. +// This Source Code Form is subject to the terms of the MIT License. // If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. // Copyright (C) Leszek Pomianowski and Fluent Framework Contributors. // All Rights Reserved. @@ -7,6 +7,7 @@ using System.Net.Http.Headers; using System.Text.Json; using Fluent.Client.AwesomeAssertions.UnitTests.Stubs; +using Xunit.Sdk; namespace Fluent.Client.AwesomeAssertions.UnitTests; @@ -15,7 +16,7 @@ public sealed class HttpResponseMessageTaskAssertionsTests [Fact] public async Task HaveStatusCode_ShouldCatchSuccess_WhenGivenRequestWithQuery() { - using System.Net.Http.HttpClient client = new( + using HttpClient client = new( new FakeHttpMessageHandler(new HttpResponseMessage(HttpStatusCode.Unauthorized)) ) { @@ -40,9 +41,7 @@ await client [Fact] public async Task Succeed_ShouldCatchSuccess_WhenServerReturnsOk() { - using System.Net.Http.HttpClient client = new( - new FakeHttpMessageHandler(new HttpResponseMessage(HttpStatusCode.OK)) - ) + using HttpClient client = new(new FakeHttpMessageHandler(new HttpResponseMessage(HttpStatusCode.OK))) { BaseAddress = new Uri("https://lepo.co"), }; @@ -53,7 +52,7 @@ public async Task Succeed_ShouldCatchSuccess_WhenServerReturnsOk() [Fact] public async Task Satisfy_ShouldVerifyResponseBody_WhenServerReturnsExpectedJson() { - using System.Net.Http.HttpClient client = new( + using HttpClient client = new( new FakeHttpMessageHandler( new HttpResponseMessage(HttpStatusCode.OK) { @@ -85,7 +84,7 @@ await client [Fact] public async Task Fail_ShouldCatchFailure_WhenServerReturnsBadRequest() { - using System.Net.Http.HttpClient client = new( + using HttpClient client = new( new FakeHttpMessageHandler( new HttpResponseMessage(HttpStatusCode.BadRequest) { ReasonPhrase = "Bad Request" } ) @@ -103,7 +102,7 @@ await client [Fact] public async Task HaveStatusCode_ShouldVerifyStatusCode_WhenServerReturnsUnauthorized() { - using System.Net.Http.HttpClient client = new( + using HttpClient client = new( new FakeHttpMessageHandler( new HttpResponseMessage(HttpStatusCode.Forbidden) { ReasonPhrase = "Unathorized" } ) @@ -118,6 +117,40 @@ await client .HaveStatusCode(HttpStatusCode.Forbidden, "because the server returned 403 Forbidden"); } + [Fact] + public async Task Satisfy_ShouldThrowJsonException_WhenServerReturnsInvalidJson() + { + using HttpClient client = new( + new FakeHttpMessageHandler( + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + "{invalid json content}", + new MediaTypeHeaderValue("application/json") + ), + } + ) + ) + { + BaseAddress = new Uri("https://lepo.co"), + }; + + Func action = async () => + await client + .Get("/v1/api/basket") + .Should() + .Satisfy( + s => + { + s.Id.Should().Be(42, "because the Id should be 42"); + s.Name.Should().Be("The Answer", "because the Name should be 'The Answer'"); + }, + "because the server returned invalid JSON body" + ); + + await action.Should().ThrowAsync(); + } + // // [Fact] // public async Task SendAsync_ShouldSatisfyAssertion_WhenBodyIsExpected() @@ -128,7 +161,7 @@ await client // new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(responseContent) } // ); // - // using var client = new System.Net.Http.HttpClient(handler) + // using var client = new HttpClient(handler) // { // BaseAddress = new Uri("https://lepo.co"), // }; @@ -160,7 +193,7 @@ await client // } // ); // - // using var client = new System.Net.Http.HttpClient(handler) + // using var client = new HttpClient(handler) // { // BaseAddress = new Uri("https://lepo.co"), // };