From 1814376a646baddf7dc861dd42f604a3b0481ee3 Mon Sep 17 00:00:00 2001 From: pomian <13592821+pomianowski@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:03:29 +0100 Subject: [PATCH] fix: Update message for invalid json --- Directory.Build.props | 12 +- Directory.Build.targets | 2 +- README.md | 143 ++++++- build/nuget.png | Bin 23302 -> 20819 bytes .../Fluent.Client.AwesomeAssertions.csproj | 1 + .../HttpResponseMessageTaskAssertions.cs | 22 +- .../HttpResponseMessageTaskExtensions.cs | 2 +- src/Fluent.Client.AwesomeAssertions/README.md | 364 ++++++++++++++++-- src/Fluent.Client/Fluent.Client.csproj | 1 + src/Fluent.Client/README.md | 268 +++++++++++-- .../HttpResponseMessageTaskAssertionsTests.cs | 53 ++- 11 files changed, 788 insertions(+), 80 deletions(-) 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 7c66e7337e59288199a479eb41a76c3d937f9534..818e367b0488f2ff1ce0c10c9e82b443ec6a2fc7 100644 GIT binary patch literal 20819 zcmcHhbySq!7d{LRASm5kf=Gyzlr++ybceKbDIi@+N`rvXf`CYOGo*l&fRx10-5@#4 zJZJoTf9rYvde^(2^}enp%rN(T&OZC>eeG-SeeQ|UP*cRmp}~PbAo$8ka#|1wI(Uf= zxrYUQoq7Mh2EWkUwG?I0W(OVzfj4(-pQ%2BK&s+#;TD+S{e3GXEma7_mlXmD4uwEa z*9UJwAfCJs$c{M#BKipep>)e^))WUHV7n<9xJd+*`7K%>G@LM%k!C2V=zu+tvRIlOY$!!2`0}c{KXTCgj5vd{U6+;gHW@Fa#Kk=>IVo1!U$53?>_llnFL7{e-$DF-SIe)a>KG zmq^)Qb+ZK2OHK1oNKP9SYQnq@Y&1z9Ce&DRA>`;Eog9G9t`fyxxt#$a$h>tS%4Yv2 z*EA1@3~-O25wI zFG4dw`}a4je-~)gU`KLu2UOBzB~M0ff0gmR!(a2;NsoG0IRWils1lCL zD6JbJN9Lt}A<6W-i}WF4Oi3kHT8GTN8fs8Rprc#Aq|}Z{t0#0nNS2m1Y)f3+@NS2z zP#)u}Sp0713onJkK+$iLblZ2)V-=#-ek-vQDOh%W9C_+9{|?Dr+I9YF*olr=4-(m$ zudcHa(v-#Lq(i6k2$PnjLnN!?fd230t~R+{>Bcm1Vi7rnz+GwT+Sc*-y67{NNFk{h z(ww@EJ(%f%VrgB{?Jx!ST@~(3pk6SDfC$Xk*Fz72n)ZT2t6eLt#qn zvr_a%L(C;%#sMv8ltm-5!F~_^-lSG77B)I4-C#i@gq4_axvyBAKgn@4lQT|4Tx@9- zs&vo$WL-e+p&L#_BwFwmbKu_>Y#tw`tC?h!Hp`d2FT=Q7x(=~9zWf+D_{{$u zMdE8bUpZh35Z~Cb`yzr#_)HNzf+^_s0Zn{FU>f3J<%y>}kJ+x(;0_OfU7+*2-5W%{ zv2c8@MEj+U?E@k{NWy$9G-*9Z#NN3o-vb(p{|rb_jR-CYX~+2M zbdS2Y)Xw4oQIqsAYp%xI-hItq+dUQcU>UYO>8?979i~*`PiSVfdtDd&_S_!OrU7-W zxt!gx|A{O~{cXAVCQhJ@&(oJpxjf^>8G|;hA2Sm@H-VhGNLtS<>u7gMr=j}vB1x~P z<6&FR;f#JjGv*cTpP3Gnn6~=Ei5qLsfR?9SXL{$Qc6Qypu#ERNLrx|@^GPksY09U2 z$Ug-NLgi`Uaj7;IHV2Qw`B5L$g)%&{Hf3(_F)^AC%{MM}GUu^lCf}MhepmeYyWXDJ z+)pf4pcau92OB&cl1zAm-(AH|i|J4KafpevJ|b2BHv!Xwo;2kCTekL_7_<)K?mi`L z1niRj5FcoGz-Yq_Uu(Q42ky1gh?%0;*jp;BW0b4!YM(tc(~=H+6D0)zlW)ZS9{Qg9}6^L-|Vn|B5#_m z2vIVcBA=~1ABv@*oeFTWSoO7Edup$TbhWw4K}{feokNcq5ObM~Bsk#j9KorX{>qM= zo~~nZ#TTFNSbh-@>gV$e&7%NX+L<{B@3VBbdLMdV)?ns8x;2)$ zQ;|sa!`XqWSH#z6l+bIK0W5rfuuJe;n4(#2#!XJ(5OZ+V%|sPDJK zVdk}#yCkW|C@P536c#=?())AWaENYnW5Wg-d6g`jAwWtXztByXXG!h+{>Mq;?57l(S``Ne`u!S~K??o~2@OS=Mv&QhVpVuA#h-?TPg`g0lTIg+( zz&bM4vGH%ky@0zP%x+zU46BY$S%tV!dP`BkJ)^%ry!<57jN;do`aRiA?&YouzpeYi zNG8;*8BY`=`q}G_b`N0Py+O<|8IN?YaxPHVXYIW9G)fI&JVKxl8u-3P-}bQ$tq|A$ ztmyf)d{lN@?0%syA6={O2SV`30g^@g+$4EuLpr&@oHdgU#6g9I)hT`y-FR7?Qw+JZ)9>#lh?9 zh9?n@2_KmQ9XGnKZ2sGhk@8H7V6BIUHRwxYk!#B354(SpOwx?`frljw;WKcz!(D4c zD|fUUVnh0Hg5Wq%oCx$1`ric&$`fB_ zuf1+qQ<3tGE8$e^WLoo#|HlmQD9lV6y_gTBbi)iU0Z~M4Z zXTj4oDc6S=`VSte!q0{h(fRl0GonyRafFlE3#HB0#;petvo3s#(Bf!EK&|*GZApOO z{!+OPxrfrnXt|ws@&`rp#-^cYvMisn?W?U8nAJv6M&$GgMjUIGh4SB{H7Ta%b1fz= z*>{u32!sEbk%TEQxo}rQ1f|9JF!0T$|E!bqiCTzhKyMCQUw8JMM9I@uGK(b*`|J^e z(Mco3%($72B@ zvKUtt{l@Y%!-7p8g=-0|ifNzI{YBiC>ejnF+6d1XGIxB4buabr&psJJ>S)-A#$I@WeMwy-kl%`|M>w} zI>GdPpwF6hwzzan_aruCl-su3@j7yfLToB%}>r9{ZM+05CK23T(i+^h22*^rj)_jOYz+IHv#8aBP!IdNQ(o zb0A1Y?gz&&fQj9o;}`|=i-EnMH1x1cEu=JXMKMiKx2)fItP6|czF#4Mg%L17nXq(K z=@9dubo9*3K#Q)Fj9jGhA5Nqo^MPiMT%bGxb3-m(T@IcdPStt!&G>Bbg3A$gHgrFo zSMPoRGy`J3W?&Eey#_c5ajht(uZ?^S)BIO0j$Ex`B8GAqnN2tAzmrxw_%Ag8rMN#Z|PKW0N_iQ7Dz)D)vN2#(^U|126HIBR)pFQh>0gZnxFj8~UbsmK6wbQh=;X21`3RS2+PJUp65!<|n` zD2`{}FqsiK|W7oA|XO^oPU2V>Xst)hXsD zAKDUm9u~-%pYy+Vx(6=&<=N{w{@C|Vb;x>9LV$j8n9`LnL5MU%RFv8GqL{aNNPUH! zf+OdJ_R(NkjaJB*zCslffFqf{wYmIX}?0fOCnM={2|3G%SQI{ zS#6vjFA)YLEV9IA_X{=%0&_L*qmf^0kHSQqkYD2Pm#rKGFVIm7C6-l;=0S?>srfg2 zLX&o=m^Tag3)AgfUdj(?Us)Q$<%4LjEA5oXG`u)2NmtKulJk+&|9;a2W32cwsGif4 zh)%lq77MLd(5SH(Q#pe4#Tvi<2(NT;($X4~i|+PjygY?#rmp(VYigC4Ph?R&I$X@m zDH{FXjLNh)cpaSBLhTFaWheFAU!o?uMUy<8BP0q0bfe|tLeE;M>#E3r!tRKTerIC595&A4d*j5G z)3(?dfIoAri%XkL1z~TD&0!v%g3?Y`!qvz9jwbJz1j@e~HLWNfhzn}d8wh6usy`w8 zM6)HP;Sc#%;n+=RcCsdwG$q0Fw(DzgR=~Nh8e8Sotrpqg`Y96F!ZYrhI6LKYei$?Z z$X|PKY4x~JZFgs48on`z)xwPSTSkIz2tB=T zEhG;V!Nr#4>C_JflpeWqDk52?wlbLAkcZ=6#x4(bd0ma^O`D+aVo_uIhQ|<@AsPUJ z*F^#epLUny_dQTAlr8zYpue2m>N^CD$dW>>c}G&~{l{9R%8}x zJVF(}`JboXTu>d4ORWF57J(~;_bl-m&afU^q;ibpYWLBN4|QaW*3F*a>4or8nAqUb z5(xzg0|f0N0S`#e*fTig16n-%OI6TG%JWL8T6R5*ZbW$bO6>XU6-tfeo!WIuTO{1@ zX3V=U>;%PTJJ#xk7L7nKlJslKj~M#oXUm~p&I0z&nY3I;>CJ-bE}#>9Q~q|(43(4B zO?P`VaW@uq(H%*!U6gxZ(Cv_C+O^sGXb7uk+W8)& zC-Ox!$3=c0#&r5dasbL>{jh%SequI)lZsqtZi3Q41BK{C7kb}YJDE@+jx2>i5ZV~q zed^Rv^v4P(Wp5hAxVp4@Oq43?hM}T|+^NFMs}mzN>2- zSo${aNG?ACcIzRJn;q16&ogWf9tD^=i;!v*LmNm!^-X3XyI#(V$a9CAZOoEs`8Kfk z*!rI9)(16QOh6`exjtPeF@2Ap^C1Pdt-adWqxv?vBBjz&Uj^r(Z01%m86T|CYF8>zr4 znhit9x2M*MKK3<=$olen{2+ug(@%m*6?wpDh(0^oxq~0 zpkfW%nH3ZYuzj<;NgJcrdM3vNDCWrqN{7?A7C?tw!*BgBsy@eZlIkx~VATD&ArkQp zwVi-q!WzC5{;6wY9b)A)^=T$ztFJl*(3ez&m^JcHB?Iz(@lV#jY1MW+u3bemF|_4< zne?GxiCgocd&*@)YwA--6d_!9T!@-!j(l3#{RBq1YlujT21$ywwE}iis$NMKd4rD- zer3RFZxI>|t+V&7dKmjs|d6zn^+- zz39;hr~FKN>1SaSP5cv^MU0(g`T*YScWQu0OAe7eOEM8IsjywH?cM4Dj$K@b!Em~{Ue--Pedlkao0+KDj?Z*da z(n^EUr$*+T;kthrB#hN;P5kunYK>qN^yD~8Kg3o*aYe)xGB)al6Fje{%aLmomj2c zIR{&OD&2(dXl*1JI0uf$xB~__$Uogm>o48`+BuoJu24neQ#|AW3ds&Aqiv42zULMO z_+pec3H(~jcMtyYa_xUXvc+E@(BC{F2Y2VXZ%lA*k7Z2El~73`P}8 ze#9K!c}a<&&QdoCC-IU@!Yg>XGecn{;Ye6e(klFL>1Q*q^_ug zPKil^0ZCLmFLmhslcIYU;5sQNPaAV1TP z(eVzP-%jhf^I7|CN}ZN}3^qEOPGo5F@LrbU@E1=2E-L>bj&mNh97R%^ri0fq?!zpYxBxdMa zg0I;!@gw+7hz|T%N}2cT(QYDG%KW&c$4akRiq&z~69K_LGS_X{PajpB1UaRX1Q{j7 zh#jcQ%UN%Yd^Qsmpm6p7L!SowDZv0TlYZwMs6&sdEq&ienI}_=e#c_j$vwy;F1H)^ zD({=Y=e%~2&nOOOfsB3G`{A2X8zUiP>GMYQ{AGvF*fL_oQCX!u^h5Wr#45DcPcrYR z#tIEFyk=QDTmbBnFF=oy#REHSe{?uvN-y}TSj6S@V9@bq))Xblbzc^1|iAcZf4&V z<*X+J0EkFgnY36st$anSeKi8Z3lRjW$BmE0u-a~RiE?t+Bkn`kb#XwjXL`Voqp!aE z;pYbmI#Kf#F#27dHVWQ3;yHmj;TKFA|~yF~$V> z=tx=FIxcl+BMG8}uvCorJJ#VXM7N(gV-eOixdGFs% zL>o?=)@#x8>UHD(fu4U$DbC>HlPU|8iPLT`fZNhiE}`%W9Srr>jL7BuRqte3r6O-G z7pO4#`*1;X#QRrvf~OqlNk@L6s?uK`i|G18o_fwBaUnq$Z}s0Im`ahkRN{ZtNNC@o zfMW-KQ}XaLp;Y4>lstG@IMnL*j9P0algknjMs*N*Z#YQTnZ5Ao*#Z%;KO54E4Tv&` ziAWh~&T?ARUJ3QtKgW~s1=+m*N8#?p9oR+6Vr;}|KPI6i0OdT$)yZSYb>vl0R<@(p zIy18t=1a;zRgj+~4uT$vW74JKYTVl04pK-da_|G|J7*N;ann9|NM~VTDYXblGJhWu zEw?zz@8@9H@Q(D$FYoc6flJDF_zghLUS3bGt9T2$N=*|w#Tt~87iqv|1>!b%>@rD0 zK2l&R^tG(t_%51%o;iM>7>gSFAi)wNH@Ode6_V?UaQQK?O0l@6!FZb{xOxiZ-2$zZ^X#UrV4_F<}c+zA{aJB zmL1N_g>klPpHy}!n#TK~WttbQdhv+&f0fi?gS7@EFwi}}nrZzGeed^Trm`zWTdd59 z3=oKua4mcm)@T@H>sLr&t~Zj+%i#4JMXq2Y7JxH13KCd&V-MPl$#fotqXjuQdoKNc zjeEZFIkBcEG(9=986^757FeoNGN4}ER@lAVGa}g8uv9J%iwnE@3)vygDV94Q_Qj+5 zu4og_{!P#Xv5HGkgAcH;%CUr6B$~^PPjr?YU7cs z@K^EIUpcJjN&t8Lc`;;gl}-JKi}$q+nL;f8{gMiTWnw%GN^R3WF|E!DrN$02plUD= zvIk~p6v$1qTlFqIy^z0HqR(p@&>f{5dj7=Ie3|5MTc@K}h>3j`Z#t$?A_C5kP(q3Y)*yx~yy>)#+^^mp(*W66F+x;)yZ8&-xPrp+#y2M-7xe8~GW-9;@wX z_9s}<&cADmb(+Z$`8XF2GY!Il1-Of{j51Xk(i>FOl0c#Dky%nz=+b0cpf69%NXR6kyG4Rk8dLm*TDhZ%ZQ$v?;wYSnQw=e!rQiBwdL?~qmh%Cu)|eD2Wo5jEW6?OCH| z97bQfQ9?r8bO~S zQk@8jreRu+#OOHM?K<8q8W$5^`X+%lAL#l>(j;rW;b+G#q3()XJBOX5SOh^jIIsfD zppr&X9^Q2E*VCC@Yzby+0lL2o0o>jDTt1`t7)aBsFsY!8Io_inD2MFic88g`A#i0% zM)rUCvRvnPeW_zy$n)r1F{G4v5(P9XBSeXZa1$F>J5N0b7LKMJhT#gYqEd|wnX3T} z_m$Cja(8_xlo8o%YdJwYhJx0SsQS_+E_LJwk`}qXG>fGz3QozJO7Z(rJX1n<7k$5c z!bJdQQI+oG=8y-mBVjWK@(ToL_12Fec3HK%?m$i+p8jAJI0;TEyolP*iyGtLwEB(V z!xQ17ep3MZI1X)nVN6;_N=aS|l~QV|OQU$NA0&4mAzpN2tSyY<8xdyhoo8Zk1D z^N9w1leb`1ame7(QUd7)csKhDJ{?mhnsfn|q#0GJq(Q8$>9BN6r&kuVsvMu^wq0Yq zH_kJR!2kNN;}Z@lGv4=Ehv$RtX0Ap8M=zFik-srQojUi2yQ}qmA?_jD3VVl2OGI** zsoFE6zFRSh^n*N#}Cy^aCr=>Wtk zB#k3ijRW-4k!QCPZcpJ2D~nFmmSekh@b~{_BudGZ-W;(MQ!65!X&MnmyZgrIt%;S!$dDP}g*|~2#@uAHJVNBE*s4JMhbJ@@MZ)%I=2e~>Rc`qycj_DaT z&iI-(?Mu(;VPXDreOOlBGzcGk)Wo2*S`|HeWWk)cAfL=f6KoMTM>9-F`Rq1X)s;bei zG)l$@QKL(#hga<8^|S^RJ`fQcx846ewb3DE%K4+d03o&DcD31_72zccI^ZJLY(T*Y z%s{_>4Jwon=2fy!7td(4CIY6f=1K|tkESi1;NrY$Lh~)n}8iG)Pz^%|k-xva~h!-odYO1|8Kmm`m0x@h&T z8(XL@yUfpm{NDJyL6xr@iH@E)LpS=5n$1Y|YtCQKWEotJl7K?=^NxiUL0m37`_w?j zz>MK2!JN;ReKH{2eEF)FdI{0+mht~=Qh>RE;KWR`-UdZtvCPY}e67bp({u?JBY)O< zj`PRwCg*9#@}`RZ??x8Tb%Z0o+*)D;YvGF{HF2u1cLJuk)!2?;^6LiI(;W;~2MZjx zhhA>+pM%%|A0<=i!pHHTfWuoC4tnBrR|+8?9hTG?S@RErsdDOv_02x6p6X)+E&44` zQp`AwOQdglpu;N6{~ghg`B}+9tWnbI0@2Z(W%ej)w3Slo3hu5y>(htnn3on@ z|GVMq48(3);c##adSgr(Lo+u}4>Wg6r=84##NpQ;X|; zzz=TaL`q#cptHVEJF*u;p{@Qw#l7pBCc}4Hw{fbV726~OP9>S6%&#cU&osQT4lNC5>}6JPTr*y~{s$n=j99Dd~8Zd;NYc-TT5eA4m2ntSsy4+a3}! zXZJa6ta)*meQ_3PtLZYjh(3uahiX}k%9AkMrC2OZ1uC`(~Tb-hT!McEB`@LW!G zZ`}lGz%?SCUdU>3p@3;x#0kb;ZVk2dTS`MdSS4%R_))%j3&X+}TxzEzz3fv1Bz2SK z&8w%sX{s1!Kb+V!i2a_+ezFu!tt=UE$QEzRdy!3IkdevcWhNXn8~Q`xFk)Y?1Y?)d z7#(q|mR%q#_jTtT@@57*D>q8#h!AvzBLDnt(|Ly75 z06KnNHG(uASkT)nq@Wj!d$k40`|xi_8r;#A-(s|HLAc|VI%h-G!GRupGfV^=c}cBF z$%Sm#omA|T@&?9*9t;YwRF>VLrZxYV|2{op{`ZYX<88YRyKeg~yhN?ReDQ(&H_nfJ zKIf{QYMG+TS1vKH9u9+wHR0ujh*ON?1HXV6tOteiM}#Pr5gtb=#b#vm`xuS{IsS1; zgjVi}#ip6|!UswQE)R~((4^ZIamG%{I|Iv72Qhn!s!IZ>1b8`VaRsAez>1OwsopAq zp~eV03vzyHiC6Awwa{WF zfILEF5rVS*A|9}F+`GZx&{aS)gCjQk*Wi&Qis|Dur>aSTj&AWa1q6t!s~=^6Vo&~rE~B&MAMoe;r7TDpEmock1=& z#ajR*m;147#|y$mnr0V+8o$kJnKxS=!8bNYpH_=JeZ;z25p}!;dsw8v{0b89#AJVz zERR+(_YmyD7Yr$U2nMVDaZ3|5w?sad3aaEi%+qt=I(vo8a~@J_MZUmaRK(Z9`j$8@ zixGy-F7*okryz-}B8~X97Y-)xUpC;6EH~4Us%U3fMB#mA z{nFMS4-s^KjOZ_KO5{mm@~;$lIl5J_sl;i>sk&NKddm zBBm`O_Z|K|QD*BkJxD_v&Sv^kx^@H*n8}?EDcIv58@hkXg+W)$3sp0yt>t_-|Mz4 zYZleGKS`Q;$q7&Iix+rc6-hJNp1wTwsha$)%?v?9jZ`oItI{~6GSyVh{>7s2| zP#=z{SkUa2%f2`9h&X3yZX{^icHID;o*2tR`oJ}&BlGT85z%H?2bK6W>-0ankv4X8 zV~l9Aw8xbEY&J^ELAq)yzcN-d@g>oXcKbM)#0>`b783G_y@6;*^Jl!f&}ltv7m1DE zm^x@-7hX!nnLHa!UYos>+xk(VcL+Dp1_M%_*+ zL_pVRr*vw{8`=fcd_`D?;-pC93Fx7Ke6hg(vR*dV>D-O4?}ftw?-2ke3f!9WFrx;D zpB>-0%;`i!C4JL{xkJNf$r_gtR4+b_V`FD^-y&|l397^ls&z#2y`HHmU?`!O#vEo+ z23zZ)sYqz}jv2D|$24A_v2+a|S2JyaE<|(Hir3CAMtwBaP2^K(iQ83=w}~Vl!$W!1 znY8I(e9ob#b;%aNKmw(J3z#qJEWi<0i2R?1xyZBw?#*EwCZjBLQrFn71Wn(wy?j{@ zk0bw=g@uaDf)XUtFVy7#I1}s(uSS59mtk)*8vv|#{fWbRnc^RhyYYP;d!4ICu7gar0xxpB|}L^U0{4XliG%sO!S8;+%)SNB(ox~21F~v)umBE>8-fEK z^Slo<&$FZ;J)St-ZBX@%2pOBpfudYZ&);!jhbf)QN3KQ2AYt7+H3I4J>b3zRnmfllL&Hm0x%_S-Sh@(@JR2=yZ&bdV1& z8G)>4D!_RCbefCD`3ES>e`)`e6~z9nY{mbgiS8}ihx6AD?}H12{Vt*${BLw0W=ekV z$`NERG+s%HMfn1#v`P+lL8Qs(5Pwa$7K2Le4G)id{se`zS5{ov z*@i>aL6OChE`8(boc)HNLC%b5`O<66#+qsw!VV>7o0s>gCd;%E!%`X7as8Ldd{WYb#_o8TjX8#9QkAW&ZEJMDqCBO)K;_6u)rs_N}4MlGCaXS!w6t& z#0(7cC995(Mx#z>nT}omU=65Vi+y+Mm2V6W)&LXdQy{IMI>)80%MWThGlOK8cuK`C^-tFxxHFr;DPW z`%UZLM1ShS*BA~kq~+GalEq5B;o;2wb&co*Er$v)p$yV!%GBkiIhC2KWJT1&A0+0> zxSro0J9lRh7Ri*Pymgu!x9qzc#MK!tAM4}|U@Wp92EssRO9e$I>oP^Rvok@f1Qb+w z0HYzsOag6(t4PWaf#9MCal z9GxKKg#GWy4|Bmq;AF(1gz<3rL(~n@QwIAz)uU>-O{t(#h-!+B;NOBwE|UGI%zrfI z!;{0!-HUx8%h5;Ys9v@lnZQ)}w_}$xS1-#knBn!mNpq`rq-+EPecClgzxb1)4wo_k z1d3$;RGXgF51U>(hMJvv($k_kMv?EvQt`7fg8^^KXt%S(0JTzLP2NAXjo2UsXh1G3 z`}5Pr|GO@w8;eeC4}m4=BlT&<``^?=N8gShK?o$)RGMiRtW#UC@w~tJoZw

@=W(Oh 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 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"), // };