From 22f9671c16c4e9b850065087e6e852b69bf7b16b Mon Sep 17 00:00:00 2001 From: nbarbettini Date: Thu, 30 Jul 2015 12:07:56 -0700 Subject: [PATCH 001/238] initial project structure --- .gitattributes | 63 +++++ .gitignore | 219 +--------------- .vs/Stormpath.Core/v14/.suo | Bin 0 -> 35328 bytes Stormpath.Core/.gitignore | 244 ++++++++++++++++++ Stormpath.Core/Stormpath.Core.sln | 34 +++ .../Stormpath.Core.v2.ncrunchsolution | Bin 0 -> 1682 bytes .../Stormpath.Core/Properties/AssemblyInfo.cs | 36 +++ .../Stormpath.Core/Stormpath.Core.csproj | 56 ++++ .../Stormpath.Core.v2.ncrunchproject | Bin 0 -> 2964 bytes .../Properties/AssemblyInfo.cs | 36 +++ .../Stormpath.Impl.Tests.csproj | 85 ++++++ .../Stormpath.Impl.Tests.v2.ncrunchproject | Bin 0 -> 2964 bytes .../Stormpath.Impl/Properties/AssemblyInfo.cs | 36 +++ .../Stormpath.Impl/Stormpath.Impl.csproj | 56 ++++ .../Stormpath.Impl.v2.ncrunchproject | Bin 0 -> 2964 bytes 15 files changed, 647 insertions(+), 218 deletions(-) create mode 100644 .gitattributes create mode 100644 .vs/Stormpath.Core/v14/.suo create mode 100644 Stormpath.Core/.gitignore create mode 100644 Stormpath.Core/Stormpath.Core.sln create mode 100644 Stormpath.Core/Stormpath.Core.v2.ncrunchsolution create mode 100644 Stormpath.Core/Stormpath.Core/Properties/AssemblyInfo.cs create mode 100644 Stormpath.Core/Stormpath.Core/Stormpath.Core.csproj create mode 100644 Stormpath.Core/Stormpath.Core/Stormpath.Core.v2.ncrunchproject create mode 100644 Stormpath.Core/Stormpath.Impl.Tests/Properties/AssemblyInfo.cs create mode 100644 Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.csproj create mode 100644 Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.v2.ncrunchproject create mode 100644 Stormpath.Core/Stormpath.Impl/Properties/AssemblyInfo.cs create mode 100644 Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj create mode 100644 Stormpath.Core/Stormpath.Impl/Stormpath.Impl.v2.ncrunchproject diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index b24f5da3..36cae91f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,221 +22,4 @@ Icon .AppleDesktop Network Trash Folder Temporary Items -.apdisk - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Windows Azure Build Output -csx/ -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions +.apdisk \ No newline at end of file diff --git a/.vs/Stormpath.Core/v14/.suo b/.vs/Stormpath.Core/v14/.suo new file mode 100644 index 0000000000000000000000000000000000000000..6babdd8c4698b35863d1e490588c17be0be2f32e GIT binary patch literal 35328 zcmeI4Uu+yl9mm%#ElFvC(tl6_bxSA(YU8+WLYn@w&vxQsC!u!SKAhAz_StcA_L(~; zY1I_@2_b}dsF2zRcqkG=>I>=<;*B>XkZ2!xM$i{t5J(7+Kx)JHGjrqH-P^mHJzso1 zXXo_W-P@g=-~4{_o8SEAH}l)~-o5kBZ@qK>UxHYAI=DIb@Y??1*3kVvUEk#Xe>w<0 zrfcqfcWtMt~1J4H~{WgRCU?N!8xg5MgQ+lU+gIl)S{NpX&`puL39{u<4 z-s}66@@?I1GoKd5Qg9)d682?XO$0BuC?)#%-r%ObFuS8qlmC5NwSC}e z{J=^utN#mvf~m`)YQ9FNgTjA@9?}=6N27f4`)^clycnzr`)OfZ3@+-pBD|-94dGuE zH`aqn{KeRR@TaB!l**W`mP4C;5H6s4O`qPUXZr=W2|gjXT>!106!83j&UXqvBe+XI zlink^S8$);vx3hF?iYMsz&l^i`HO-F1pR^s1rG_nBseJevVeS9+8qvbvj3z1o29eg z{m(1({{dy%OZ}gILO-I#n$aJA*2(_Qm`wll#UJkfPRef4`{)7s-dCz+xo6!tKcYH3 zEcmM6QNh;*)a`3JKOy*rU`T*|9u_<%7!f=!I4n3K_@>~ffcK8+{G{M1!PA0g1kVaa z1!IDo;6~IteWDxrf%YUo|8qaC{~5y>L(}Mg#tR?)PrD<7$N=q6o74Wt0lH&Q;M(8S znf7;G-_|J3u34)^~)<|D{HsbNP6J=kmgpYfZynJ@j}{y#4bGiP*Q zF*v95%hEvT|I5-xi@Iu+9+LjU7QlY=#s8f0qO(YhJIuAqn(HoT{FxNqqW)JjC$^q} zg6gp;`;6HzIXTv};uF$;52zgGl)m^G%fU?=SGj1~wQ9*P2>*Jpq`OyCgOV_<3SZ;7 z_>k~p8!_+o#XqII&i~L1{~2M2Gvne6K8d6>DE!!9%)Nc_hv}acjYTukSQ;&#mqqc4 z?v@Q^`y>fFHrY*mcj*zfoiF}pl=m`1t8_|IcOjS&wsKt?&l?Ng=4*76{Pp3ZWIXf5 zkNsaoFPufuSkZ4@G#AwBjU+9(2JP4%Vg8?yw4rquBy$BpRO)MTo{SyeNqYZJ8vL#N zaSw}=_;~R5`0_su{&8{TvSxYA?UIt@&)@|&DJf6A$GF&k64(aeZ^!A6zz;xzoU=%8>*5*YVyLKRyEVB?TiR7WVsRkG1Z}PCK#dh=`yq~KQ633q`f8m-$IWVI3ho7+1m;d-m z8@0d1ga)vlh*3%6hU^2ZKP)lshST9TW5s6YfKo^^W$1Qmj;ILwP`1XD4~VWedmmJ4ez~RCV^frnfWmt z(%Gr4__Ac`PwAv23{Lmk9JS-|vFR!7NNbaC2xD1wW>!r1AJq|m1XfW|+(SpRW5c!t z@8Z`&80-0YNo#mVHR>Kw&clYo1L9~w*F#b-BRU?@ zZ$Lced`?%(IuGl2NGfPSZ!(`-(7(>tY)GXamJ|$0E*=+PBaEmf*bO6kHY}iK<3<2e~Uoow4QJ$N5!+*wEtgELq&mK|!#|6|d-rl3a%e)Y2Wn}`X=sbrf z)RvFZ-$-J!c4r|RZuijpeTp^#xDYKCN7)M;cm{AFf%7?G8q<1#;c0IS7wM0I` z`Ml~?QHk^ws~ZL-&*&ty$%@YOG1}Q_J18vl6os?W02mQyg~4qZ_M!7^57XJY6|eL# zbxsr2+LM)pP4!Rf;kOpO-P7jN(yKkpi<5@(K^fHO%niwS>$YF&VS4Q@MjtABSx+$m zVm$DnvzMjUwI+KK)`RbO(R-n%q_=*|X5PWx3p;Ub)xy{n4z^NLUXQ)stUgs_S23>l zo0-og=?pBheq&<}%DS<3owZx$^aS4|HX-vQ>pNLAR*GFoWrSrFp{l8h$31n-&uS?2x@F2duExw^~$Y z`d~pQ^}1CS*LKW^qjsCikwp28X34PsAj+LI_D~WBdwHa+(QfUYdf9O~k|;kbUfP*& zFU;1WtzNV9!uOM?>}3m~k$sC_EKE1v2S$jRC7i^Gy<_>Z6DUf*wWX4%?&bDxWFgkK ze>`WL6V8gNoo90s@=1%4S=uEIxV=zebUV2hF30y83u{Y|PVd*-WWJX4yfEdB)w?K5 z7F!#iY7l(C>vdR^HxM6U5d5&~b+MkvqU`;T25&0HZ}`!u3(KCCFI&6bPWYRVk&4cX zU6Pe}9hjFc$$LK1C0VJj59^A^%8$CMQNR9Rq_!h-{;H=>K?lWmzoA`XBan9r? z@x3!?%VTE>KuY^A@5oNSEof8;HafOQQ+m#2aw0QisEyNIUHQbuSGg{3+ zR82V620f2Xv$i+d7X4P+3)E;+S$$4c`|a9H-c+C|A#C`8)hF1g6aKN!#QvIJxm@n7 zvbvV6XBuxj8kqKD1!Ge>d1HImDpBt@o3Ha6lXVsZuWEn7sCH5u(c0{!Ru@LKkHYSC zI%;-V9MwH`*c_FGFsd8_$~Ua5L$Vpz9n1cgoZc7-UNdV$S7LZ=)PThbV~&{!vE;hh z=q~c^Gx7eNtlKfwxzp6LFNBp3VulJqd?(qUMj#?Oh~7P~tDue>xanOJ(4@c7(i{umA7ZtvRp#do2j{d7>R6pa99 z#cbZ_0d`}t7ra@*Pr^)Ww>Qzc(E>#V9L?u9D$Ds3rL{tSytH^}t+2jX$>&N-g?weR zR9@Ry*gQW_S-Lo|SUJB?-pCKGtZuHZte46xGX%FjxqhiwBrYy(`om8idHdB5uKcat z{kLiHW%)bHf#mqFRPmoC_K0!FcjI=a<3GVm46-l&4#$6*_>Wss;19?DbRhmSvU%P| zI>Pun5dUrBZ?R#0`Tv~qx_N`!9kzq`ZxegW`l~PgF#YjBhj2WMZ~XV&RouS#vAG#H zNzL%PeZOw4)V>it68}x??S6qT{xJQmdqZb^qUM@xNGr$UaGzEz@3$|0VvF9TvXyH;eymBmT4R9^rl<1^yd7{&P^g%Hlu8 z1f?wg)AC^_N2Bv;_sPJyZGBU{&wR3)6&m)(aHFKzJG!} z=c|9i_D7ul8QD9#8ULTf|2j{K8nBI&#sAtbl(ZKfj!!u1^Uvactq^AMzghgR+YRUZ z0IZi~@xNL8Zx;VcRD3VT1~iJ|zR{v)nnkqt^8WaE)I+OyLSn^ODNeWhxtrf8NQ!ds zp|Y2wnsU`WCO&lbvh>DdHhk#4&{NV|A2$$<vGFrG6>OYR-ZH;ey`w}0H0jJJQ-lCsxX{BKlOT>pPNiS( iS^RHOY;qR=+bkBDU9egFZx;WHR>|UjlXUBiaPR-yeis!0 literal 0 HcmV?d00001 diff --git a/Stormpath.Core/.gitignore b/Stormpath.Core/.gitignore new file mode 100644 index 00000000..59a8893c --- /dev/null +++ b/Stormpath.Core/.gitignore @@ -0,0 +1,244 @@ +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* +.ncrunchproject +.ncrunchsolution + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions diff --git a/Stormpath.Core/Stormpath.Core.sln b/Stormpath.Core/Stormpath.Core.sln new file mode 100644 index 00000000..0297f04f --- /dev/null +++ b/Stormpath.Core/Stormpath.Core.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.Core", "Stormpath.Core\Stormpath.Core.csproj", "{79A65C37-9DB1-413A-AC23-708404530295}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.Impl", "Stormpath.Impl\Stormpath.Impl.csproj", "{AD169335-C85D-4A19-8A76-F63E48972F10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.Impl.Tests", "Stormpath.Impl.Tests\Stormpath.Impl.Tests.csproj", "{091F6F3E-304A-47B5-AA4A-79684560856C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {79A65C37-9DB1-413A-AC23-708404530295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79A65C37-9DB1-413A-AC23-708404530295}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79A65C37-9DB1-413A-AC23-708404530295}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79A65C37-9DB1-413A-AC23-708404530295}.Release|Any CPU.Build.0 = Release|Any CPU + {AD169335-C85D-4A19-8A76-F63E48972F10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD169335-C85D-4A19-8A76-F63E48972F10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD169335-C85D-4A19-8A76-F63E48972F10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD169335-C85D-4A19-8A76-F63E48972F10}.Release|Any CPU.Build.0 = Release|Any CPU + {091F6F3E-304A-47B5-AA4A-79684560856C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {091F6F3E-304A-47B5-AA4A-79684560856C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {091F6F3E-304A-47B5-AA4A-79684560856C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {091F6F3E-304A-47B5-AA4A-79684560856C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Stormpath.Core/Stormpath.Core.v2.ncrunchsolution b/Stormpath.Core/Stormpath.Core.v2.ncrunchsolution new file mode 100644 index 0000000000000000000000000000000000000000..f7414fb45cf9891dbe942c670a9aea493e72d256 GIT binary patch literal 1682 zcmcJQ+fKqj5QhJ26W_r%FnUdz5EWx0ga{sPu_>bVVA>-3^6EDOrC{U&3n8VuJ2U%# zJ3G7m{%UEUOtGda^-L?(ng5UJJu#YVw6p{|)Paupwba2LE7BcLu3`}z=mgvGMr$2^ zh0@q#`^0B@p&g=h!+Sm+;Stg+bnEk{@U&?aj3s(R{Zl>Y688y+8Zl;8BiQ}?HG3PK z80MM!Xj+D>AMp&GrPPH!XoV|T4Om-Fe@^$m8U7-u6brO7vNt7bw)Wp-_v}!q30@Nz z63W=5i_G<=fu2*)}sxIa!$YNE2Lw5EJua0~5@JgLN@Jfhujy=Uc zVKk&BtZmg`Rah+Q&vvDWQd8 + + + + Debug + AnyCPU + {79A65C37-9DB1-413A-AC23-708404530295} + Library + Properties + Stormpath.Core + Stormpath.Core + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Stormpath.Core/Stormpath.Core/Stormpath.Core.v2.ncrunchproject b/Stormpath.Core/Stormpath.Core/Stormpath.Core.v2.ncrunchproject new file mode 100644 index 0000000000000000000000000000000000000000..f14526cf83386d3d740ab04ba7a6314f48bd1326 GIT binary patch literal 2964 zcmb_e%Wm614CJ{${~>?S^it$t7*LZOawuZN4th>yIZb04Qv9l9hW)=4^9lc+ z!I68%u)V_`?k4tUd2RB{;Xeg0vb<83%vTKDm411_ml@s)*{GEL1gscCihcK;ERXkT z9C?}v*c3pW_s-~c?hf^0JR)!)i&we8$>v~_Ap^0V3bW^R=D?PxN8LSOHI}DMp7?Er zHJ;`1-ny7gf$Q}>gjZ8##_TX}UA&kZ<|xvv`(jL!{DH`o4=)cVAwu!6{>9d93tw*La2+(3l=HiqHr zNjhu@>WDH>l8^R$3FxXI$|{VWV2aP%iL zMZDCK?`YSEtc}*JlHvA-U9W;fF69jo#)ve`r5SiIYU-t)*CBR>b^%Y+J$sU`X!fLQ zRb`1teNSs5^|dh+=HJf@)_J{iv}a(YbEa$F)ua6!kH(NO@eHY1x0z@m*HpVhzJIJ| z{i$~8el~`2i!-a&zMl!*cMnd1A^UfuOb`*f{z7|UhA6sxw|ALGs=0s;-`97iP~n{P z4xQC|{CJmn^pTyz-z{pXgf`fpgAREQ!|IbdjJ;qSGeQTpea__@P7k+hc)feTUo~`J0?UYm9Zct>M<^)_juz_KmQ|-){X} e4H>EsIp?773B2i^fQQ+T)qc<1;nmRRmi-^Q0vlie literal 0 HcmV?d00001 diff --git a/Stormpath.Core/Stormpath.Impl.Tests/Properties/AssemblyInfo.cs b/Stormpath.Core/Stormpath.Impl.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..00dfc1c5 --- /dev/null +++ b/Stormpath.Core/Stormpath.Impl.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Stormpath.Impl.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stormpath.Impl.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("091f6f3e-304a-47b5-aa4a-79684560856c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.csproj b/Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.csproj new file mode 100644 index 00000000..cf320b24 --- /dev/null +++ b/Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.csproj @@ -0,0 +1,85 @@ + + + + Debug + AnyCPU + {091F6F3E-304A-47B5-AA4A-79684560856C} + Library + Properties + Stormpath.Impl.Tests + Stormpath.Impl.Tests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.v2.ncrunchproject b/Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.v2.ncrunchproject new file mode 100644 index 0000000000000000000000000000000000000000..f14526cf83386d3d740ab04ba7a6314f48bd1326 GIT binary patch literal 2964 zcmb_e%Wm614CJ{${~>?S^it$t7*LZOawuZN4th>yIZb04Qv9l9hW)=4^9lc+ z!I68%u)V_`?k4tUd2RB{;Xeg0vb<83%vTKDm411_ml@s)*{GEL1gscCihcK;ERXkT z9C?}v*c3pW_s-~c?hf^0JR)!)i&we8$>v~_Ap^0V3bW^R=D?PxN8LSOHI}DMp7?Er zHJ;`1-ny7gf$Q}>gjZ8##_TX}UA&kZ<|xvv`(jL!{DH`o4=)cVAwu!6{>9d93tw*La2+(3l=HiqHr zNjhu@>WDH>l8^R$3FxXI$|{VWV2aP%iL zMZDCK?`YSEtc}*JlHvA-U9W;fF69jo#)ve`r5SiIYU-t)*CBR>b^%Y+J$sU`X!fLQ zRb`1teNSs5^|dh+=HJf@)_J{iv}a(YbEa$F)ua6!kH(NO@eHY1x0z@m*HpVhzJIJ| z{i$~8el~`2i!-a&zMl!*cMnd1A^UfuOb`*f{z7|UhA6sxw|ALGs=0s;-`97iP~n{P z4xQC|{CJmn^pTyz-z{pXgf`fpgAREQ!|IbdjJ;qSGeQTpea__@P7k+hc)feTUo~`J0?UYm9Zct>M<^)_juz_KmQ|-){X} e4H>EsIp?773B2i^fQQ+T)qc<1;nmRRmi-^Q0vlie literal 0 HcmV?d00001 diff --git a/Stormpath.Core/Stormpath.Impl/Properties/AssemblyInfo.cs b/Stormpath.Core/Stormpath.Impl/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..6b465ec1 --- /dev/null +++ b/Stormpath.Core/Stormpath.Impl/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Stormpath.Impl")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stormpath.Impl")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ad169335-c85d-4a19-8a76-f63e48972f10")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj b/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj new file mode 100644 index 00000000..21d0d7a5 --- /dev/null +++ b/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj @@ -0,0 +1,56 @@ + + + + + Debug + AnyCPU + {AD169335-C85D-4A19-8A76-F63E48972F10} + Library + Properties + Stormpath.Impl + Stormpath.Impl + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.v2.ncrunchproject b/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.v2.ncrunchproject new file mode 100644 index 0000000000000000000000000000000000000000..f14526cf83386d3d740ab04ba7a6314f48bd1326 GIT binary patch literal 2964 zcmb_e%Wm614CJ{${~>?S^it$t7*LZOawuZN4th>yIZb04Qv9l9hW)=4^9lc+ z!I68%u)V_`?k4tUd2RB{;Xeg0vb<83%vTKDm411_ml@s)*{GEL1gscCihcK;ERXkT z9C?}v*c3pW_s-~c?hf^0JR)!)i&we8$>v~_Ap^0V3bW^R=D?PxN8LSOHI}DMp7?Er zHJ;`1-ny7gf$Q}>gjZ8##_TX}UA&kZ<|xvv`(jL!{DH`o4=)cVAwu!6{>9d93tw*La2+(3l=HiqHr zNjhu@>WDH>l8^R$3FxXI$|{VWV2aP%iL zMZDCK?`YSEtc}*JlHvA-U9W;fF69jo#)ve`r5SiIYU-t)*CBR>b^%Y+J$sU`X!fLQ zRb`1teNSs5^|dh+=HJf@)_J{iv}a(YbEa$F)ua6!kH(NO@eHY1x0z@m*HpVhzJIJ| z{i$~8el~`2i!-a&zMl!*cMnd1A^UfuOb`*f{z7|UhA6sxw|ALGs=0s;-`97iP~n{P z4xQC|{CJmn^pTyz-z{pXgf`fpgAREQ!|IbdjJ;qSGeQTpea__@P7k+hc)feTUo~`J0?UYm9Zct>M<^)_juz_KmQ|-){X} e4H>EsIp?773B2i^fQQ+T)qc<1;nmRRmi-^Q0vlie literal 0 HcmV?d00001 From bf0675398ad32230eb289b6b0596fa62ab299b89 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 30 Jul 2015 12:16:38 -0700 Subject: [PATCH 002/238] cleanup --- .../Stormpath.Impl.Tests.v2.ncrunchproject | Bin 2964 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.v2.ncrunchproject diff --git a/Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.v2.ncrunchproject b/Stormpath.Core/Stormpath.Impl.Tests/Stormpath.Impl.Tests.v2.ncrunchproject deleted file mode 100644 index f14526cf83386d3d740ab04ba7a6314f48bd1326..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2964 zcmb_e%Wm614CJ{${~>?S^it$t7*LZOawuZN4th>yIZb04Qv9l9hW)=4^9lc+ z!I68%u)V_`?k4tUd2RB{;Xeg0vb<83%vTKDm411_ml@s)*{GEL1gscCihcK;ERXkT z9C?}v*c3pW_s-~c?hf^0JR)!)i&we8$>v~_Ap^0V3bW^R=D?PxN8LSOHI}DMp7?Er zHJ;`1-ny7gf$Q}>gjZ8##_TX}UA&kZ<|xvv`(jL!{DH`o4=)cVAwu!6{>9d93tw*La2+(3l=HiqHr zNjhu@>WDH>l8^R$3FxXI$|{VWV2aP%iL zMZDCK?`YSEtc}*JlHvA-U9W;fF69jo#)ve`r5SiIYU-t)*CBR>b^%Y+J$sU`X!fLQ zRb`1teNSs5^|dh+=HJf@)_J{iv}a(YbEa$F)ua6!kH(NO@eHY1x0z@m*HpVhzJIJ| z{i$~8el~`2i!-a&zMl!*cMnd1A^UfuOb`*f{z7|UhA6sxw|ALGs=0s;-`97iP~n{P z4xQC|{CJmn^pTyz-z{pXgf`fpgAREQ!|IbdjJ;qSGeQTpea__@P7k+hc)feTUo~`J0?UYm9Zct>M<^)_juz_KmQ|-){X} e4H>EsIp?773B2i^fQQ+T)qc<1;nmRRmi-^Q0vlie From e76bde425265b47a65cd29403ed4228be81e5992 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 30 Jul 2015 12:16:57 -0700 Subject: [PATCH 003/238] cleanup --- .../Stormpath.Core.v2.ncrunchproject | Bin 2964 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Stormpath.Core/Stormpath.Core/Stormpath.Core.v2.ncrunchproject diff --git a/Stormpath.Core/Stormpath.Core/Stormpath.Core.v2.ncrunchproject b/Stormpath.Core/Stormpath.Core/Stormpath.Core.v2.ncrunchproject deleted file mode 100644 index f14526cf83386d3d740ab04ba7a6314f48bd1326..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2964 zcmb_e%Wm614CJ{${~>?S^it$t7*LZOawuZN4th>yIZb04Qv9l9hW)=4^9lc+ z!I68%u)V_`?k4tUd2RB{;Xeg0vb<83%vTKDm411_ml@s)*{GEL1gscCihcK;ERXkT z9C?}v*c3pW_s-~c?hf^0JR)!)i&we8$>v~_Ap^0V3bW^R=D?PxN8LSOHI}DMp7?Er zHJ;`1-ny7gf$Q}>gjZ8##_TX}UA&kZ<|xvv`(jL!{DH`o4=)cVAwu!6{>9d93tw*La2+(3l=HiqHr zNjhu@>WDH>l8^R$3FxXI$|{VWV2aP%iL zMZDCK?`YSEtc}*JlHvA-U9W;fF69jo#)ve`r5SiIYU-t)*CBR>b^%Y+J$sU`X!fLQ zRb`1teNSs5^|dh+=HJf@)_J{iv}a(YbEa$F)ua6!kH(NO@eHY1x0z@m*HpVhzJIJ| z{i$~8el~`2i!-a&zMl!*cMnd1A^UfuOb`*f{z7|UhA6sxw|ALGs=0s;-`97iP~n{P z4xQC|{CJmn^pTyz-z{pXgf`fpgAREQ!|IbdjJ;qSGeQTpea__@P7k+hc)feTUo~`J0?UYm9Zct>M<^)_juz_KmQ|-){X} e4H>EsIp?773B2i^fQQ+T)qc<1;nmRRmi-^Q0vlie From 7c6f3d24672ad60d3424865c93451087582c45b3 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 30 Jul 2015 12:17:07 -0700 Subject: [PATCH 004/238] cleanup --- .../Stormpath.Impl.v2.ncrunchproject | Bin 2964 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Stormpath.Core/Stormpath.Impl/Stormpath.Impl.v2.ncrunchproject diff --git a/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.v2.ncrunchproject b/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.v2.ncrunchproject deleted file mode 100644 index f14526cf83386d3d740ab04ba7a6314f48bd1326..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2964 zcmb_e%Wm614CJ{${~>?S^it$t7*LZOawuZN4th>yIZb04Qv9l9hW)=4^9lc+ z!I68%u)V_`?k4tUd2RB{;Xeg0vb<83%vTKDm411_ml@s)*{GEL1gscCihcK;ERXkT z9C?}v*c3pW_s-~c?hf^0JR)!)i&we8$>v~_Ap^0V3bW^R=D?PxN8LSOHI}DMp7?Er zHJ;`1-ny7gf$Q}>gjZ8##_TX}UA&kZ<|xvv`(jL!{DH`o4=)cVAwu!6{>9d93tw*La2+(3l=HiqHr zNjhu@>WDH>l8^R$3FxXI$|{VWV2aP%iL zMZDCK?`YSEtc}*JlHvA-U9W;fF69jo#)ve`r5SiIYU-t)*CBR>b^%Y+J$sU`X!fLQ zRb`1teNSs5^|dh+=HJf@)_J{iv}a(YbEa$F)ua6!kH(NO@eHY1x0z@m*HpVhzJIJ| z{i$~8el~`2i!-a&zMl!*cMnd1A^UfuOb`*f{z7|UhA6sxw|ALGs=0s;-`97iP~n{P z4xQC|{CJmn^pTyz-z{pXgf`fpgAREQ!|IbdjJ;qSGeQTpea__@P7k+hc)feTUo~`J0?UYm9Zct>M<^)_juz_KmQ|-){X} e4H>EsIp?773B2i^fQQ+T)qc<1;nmRRmi-^Q0vlie From f89ac4302d2244f2a8e02f0031957de6737c1cce Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 30 Jul 2015 12:17:23 -0700 Subject: [PATCH 005/238] cleanup --- Stormpath.Core/Stormpath.Core.v2.ncrunchsolution | Bin 1682 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Stormpath.Core/Stormpath.Core.v2.ncrunchsolution diff --git a/Stormpath.Core/Stormpath.Core.v2.ncrunchsolution b/Stormpath.Core/Stormpath.Core.v2.ncrunchsolution deleted file mode 100644 index f7414fb45cf9891dbe942c670a9aea493e72d256..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1682 zcmcJQ+fKqj5QhJ26W_r%FnUdz5EWx0ga{sPu_>bVVA>-3^6EDOrC{U&3n8VuJ2U%# zJ3G7m{%UEUOtGda^-L?(ng5UJJu#YVw6p{|)Paupwba2LE7BcLu3`}z=mgvGMr$2^ zh0@q#`^0B@p&g=h!+Sm+;Stg+bnEk{@U&?aj3s(R{Zl>Y688y+8Zl;8BiQ}?HG3PK z80MM!Xj+D>AMp&GrPPH!XoV|T4Om-Fe@^$m8U7-u6brO7vNt7bw)Wp-_v}!q30@Nz z63W=5i_G<=fu2*)}sxIa!$YNE2Lw5EJua0~5@JgLN@Jfhujy=Uc zVKk&BtZmg`Rah+Q&vvDWQd8 Date: Thu, 30 Jul 2015 12:36:57 -0700 Subject: [PATCH 006/238] exclude ncrunch --- Stormpath.Core/.gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Stormpath.Core/.gitignore b/Stormpath.Core/.gitignore index 59a8893c..28ebd526 100644 --- a/Stormpath.Core/.gitignore +++ b/Stormpath.Core/.gitignore @@ -135,8 +135,8 @@ _TeamCity* _NCrunch_* .*crunch*.local.xml nCrunchTemp_* -.ncrunchproject -.ncrunchsolution +*.ncrunchproject +*.ncrunchsolution # MightyMoose *.mm.* From e4a9f2426bbf4b770706efcbfdbfec9890fbe530 Mon Sep 17 00:00:00 2001 From: nbarbettini Date: Thu, 30 Jul 2015 12:37:12 -0700 Subject: [PATCH 007/238] more initial setup --- Stormpath.Core/Stormpath.Impl/packages.config | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Stormpath.Core/Stormpath.Impl/packages.config diff --git a/Stormpath.Core/Stormpath.Impl/packages.config b/Stormpath.Core/Stormpath.Impl/packages.config new file mode 100644 index 00000000..9396df9c --- /dev/null +++ b/Stormpath.Core/Stormpath.Impl/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From f706b205f25d11d5ca17a99ee10108b330fc1696 Mon Sep 17 00:00:00 2001 From: nbarbettini Date: Thu, 30 Jul 2015 12:38:00 -0700 Subject: [PATCH 008/238] more initial setup --- Stormpath.Core/Stormpath.Core.sln | 2 +- Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj | 7 +++++++ .../Properties/AssemblyInfo.cs | 0 .../Stormpath.SDK.csproj} | 3 +++ 4 files changed, 11 insertions(+), 1 deletion(-) rename Stormpath.Core/{Stormpath.Core => Stormpath.SDK}/Properties/AssemblyInfo.cs (100%) rename Stormpath.Core/{Stormpath.Core/Stormpath.Core.csproj => Stormpath.SDK/Stormpath.SDK.csproj} (97%) diff --git a/Stormpath.Core/Stormpath.Core.sln b/Stormpath.Core/Stormpath.Core.sln index 0297f04f..c2f941ba 100644 --- a/Stormpath.Core/Stormpath.Core.sln +++ b/Stormpath.Core/Stormpath.Core.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.Core", "Stormpath.Core\Stormpath.Core.csproj", "{79A65C37-9DB1-413A-AC23-708404530295}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK", "Stormpath.SDK\Stormpath.SDK.csproj", "{79A65C37-9DB1-413A-AC23-708404530295}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.Impl", "Stormpath.Impl\Stormpath.Impl.csproj", "{AD169335-C85D-4A19-8A76-F63E48972F10}" EndProject diff --git a/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj b/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj index 21d0d7a5..1d1e0dc7 100644 --- a/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj +++ b/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj @@ -33,6 +33,10 @@ false + + ..\packages\RestSharp.105.1.0\lib\net45\RestSharp.dll + True + @@ -45,6 +49,9 @@ + + + - \ No newline at end of file diff --git a/Stormpath.Core/Stormpath.Impl/Properties/AssemblyInfo.cs b/Stormpath.Core/Stormpath.Impl/Properties/AssemblyInfo.cs deleted file mode 100644 index 6b465ec1..00000000 --- a/Stormpath.Core/Stormpath.Impl/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Stormpath.Impl")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Stormpath.Impl")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("ad169335-c85d-4a19-8a76-f63e48972f10")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj b/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj deleted file mode 100644 index 1d1e0dc7..00000000 --- a/Stormpath.Core/Stormpath.Impl/Stormpath.Impl.csproj +++ /dev/null @@ -1,63 +0,0 @@ - - - - - Debug - AnyCPU - {AD169335-C85D-4A19-8A76-F63E48972F10} - Library - Properties - Stormpath.Impl - Stormpath.Impl - v4.5 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - ..\packages\RestSharp.105.1.0\lib\net45\RestSharp.dll - True - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Stormpath.Core/Stormpath.Impl/packages.config b/Stormpath.Core/Stormpath.Impl/packages.config deleted file mode 100644 index 9396df9c..00000000 --- a/Stormpath.Core/Stormpath.Impl/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Stormpath.Core/.gitignore b/Stormpath.SDK/.gitignore similarity index 100% rename from Stormpath.Core/.gitignore rename to Stormpath.SDK/.gitignore diff --git a/Stormpath.SDK/Stormpath.Core.sln b/Stormpath.SDK/Stormpath.Core.sln new file mode 100644 index 00000000..99b4039e --- /dev/null +++ b/Stormpath.SDK/Stormpath.Core.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK", "Stormpath.SDK\Stormpath.SDK.csproj", "{79A65C37-9DB1-413A-AC23-708404530295}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {79A65C37-9DB1-413A-AC23-708404530295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79A65C37-9DB1-413A-AC23-708404530295}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79A65C37-9DB1-413A-AC23-708404530295}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79A65C37-9DB1-413A-AC23-708404530295}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Stormpath.SDK/Stormpath.SDK/Api/IApiKey.cs b/Stormpath.SDK/Stormpath.SDK/Api/IApiKey.cs new file mode 100644 index 00000000..747a74e6 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Api/IApiKey.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Api +{ + public interface IApiKey + { + Task GetIdAsync(); + + Task GetSecretAsync(); + + // ApiKeyStatus + + //void SetStatus() + + //Task + + //Task + + // void Save() + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Api/IApiKeyBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Api/IApiKeyBuilder.cs new file mode 100644 index 00000000..531966f7 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Api/IApiKeyBuilder.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Api +{ + public interface IApiKeyBuilder + { + + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs b/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs new file mode 100644 index 00000000..c495fbdd --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Client +{ + public interface IClient + { + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs new file mode 100644 index 00000000..24578800 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs @@ -0,0 +1,15 @@ +namespace Stormpath.SDK.Client +{ + public interface IClientBuilder + { + IClientBuilder SetApiKey(ApiKey apiKey); + + IClientBuilder SetAuthenticationScheme(AuthenticationScheme authenticationScheme); + + IClientBuilder SetConnectionTimeout(int timeout); + + IClientBuilder SetBaseUrl(string baseUrl); + + IClient Build(); + } +} diff --git a/Stormpath.Core/Stormpath.SDK/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs similarity index 100% rename from Stormpath.Core/Stormpath.SDK/Properties/AssemblyInfo.cs rename to Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs diff --git a/Stormpath.Core/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj similarity index 88% rename from Stormpath.Core/Stormpath.SDK/Stormpath.SDK.csproj rename to Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 78cc025e..edffd7a5 100644 --- a/Stormpath.Core/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -7,8 +7,8 @@ {79A65C37-9DB1-413A-AC23-708404530295} Library Properties - Stormpath.Core - Stormpath.Core + Stormpath.SDK + Stormpath.SDK v4.5 512 @@ -22,6 +22,7 @@ prompt 4 false + true pdbonly @@ -43,11 +44,12 @@ + + + + - - - + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.Core.sln b/Stormpath.SDK/Stormpath.SDK.sln similarity index 66% rename from Stormpath.SDK/Stormpath.Core.sln rename to Stormpath.SDK/Stormpath.SDK.sln index 99b4039e..8a8efa14 100644 --- a/Stormpath.SDK/Stormpath.Core.sln +++ b/Stormpath.SDK/Stormpath.SDK.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK", "Stormpath.SDK\Stormpath.SDK.csproj", "{79A65C37-9DB1-413A-AC23-708404530295}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.Tests", "Stormpath.SDK.Tests\Stormpath.SDK.Tests.csproj", "{3579A409-6C66-4181-8268-C851878E5E22}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {79A65C37-9DB1-413A-AC23-708404530295}.Debug|Any CPU.Build.0 = Debug|Any CPU {79A65C37-9DB1-413A-AC23-708404530295}.Release|Any CPU.ActiveCfg = Release|Any CPU {79A65C37-9DB1-413A-AC23-708404530295}.Release|Any CPU.Build.0 = Release|Any CPU + {3579A409-6C66-4181-8268-C851878E5E22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3579A409-6C66-4181-8268-C851878E5E22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3579A409-6C66-4181-8268-C851878E5E22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3579A409-6C66-4181-8268-C851878E5E22}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Stormpath.SDK/Stormpath.SDK/Api/ApiKeys.cs b/Stormpath.SDK/Stormpath.SDK/Api/ClientApiKeys.cs similarity index 54% rename from Stormpath.SDK/Stormpath.SDK/Api/ApiKeys.cs rename to Stormpath.SDK/Stormpath.SDK/Api/ClientApiKeys.cs index 7956f13e..a4edfc30 100644 --- a/Stormpath.SDK/Stormpath.SDK/Api/ApiKeys.cs +++ b/Stormpath.SDK/Stormpath.SDK/Api/ClientApiKeys.cs @@ -6,11 +6,11 @@ namespace Stormpath.SDK.Api { - public sealed class ApiKeys + public sealed class ClientApiKeys { - public static IApiKeyBuilder Builder() + public static IClientApiKeyBuilder Builder() { - return new Impl.Api.ClientApiKeyBuilder(); + return new Impl.Api.DefaultClientApiKeyBuilder(); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Api/IApiKey.cs b/Stormpath.SDK/Stormpath.SDK/Api/IApiKey.cs deleted file mode 100644 index c1a82af4..00000000 --- a/Stormpath.SDK/Stormpath.SDK/Api/IApiKey.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Stormpath.SDK.Api -{ - public interface IApiKey - { - Task GetIdAsync(); - - Task GetSecretAsync(); - - // begin TODO - // ApiKeyStatus - - //void SetStatus() - - //Task - - //Task - - // void Save() - // end TODO - } -} diff --git a/Stormpath.SDK/Stormpath.SDK/Api/IApiKeyBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Api/IApiKeyBuilder.cs deleted file mode 100644 index 7357b3de..00000000 --- a/Stormpath.SDK/Stormpath.SDK/Api/IApiKeyBuilder.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Stormpath.SDK.Api -{ - public interface IApiKeyBuilder - { - IApiKeyBuilder SetId(string id); - - IApiKeyBuilder SetSecret(string secret); - - // begin TODO - // setProperties - // setReader - // setInputString - // setFileLocaiton - // setIdPropertyName - // setSecretPropertyName - // end TODO - - IApiKey Build(); - } -} diff --git a/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKey.cs b/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKey.cs new file mode 100644 index 00000000..21722d22 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKey.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Api +{ + public interface IClientApiKey + { + string GetId(); + + string GetSecret(); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKeyBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKeyBuilder.cs new file mode 100644 index 00000000..fc510db1 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKeyBuilder.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Api +{ + public interface IClientApiKeyBuilder + { + IClientApiKeyBuilder SetId(string id); + + IClientApiKeyBuilder SetSecret(string secret); + + IClientApiKeyBuilder SetInputStream(System.IO.Stream stream); + + IClientApiKeyBuilder SetFileLocation(string path); + + IClientApiKeyBuilder SetIdPropertyName(string idPropertyName); + + IClientApiKeyBuilder SetSecretPropertyName(string secretPropertyName); + + IClientApiKey Build(); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs index 3f61f6f2..79d27216 100644 --- a/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs @@ -4,7 +4,7 @@ namespace Stormpath.SDK.Client { public interface IClientBuilder { - IClientBuilder SetApiKey(IApiKey apiKey); + IClientBuilder SetApiKey(IClientApiKey apiKey); //IClientBuilder SetAuthenticationScheme(AuthenticationScheme authenticationScheme); // TODO diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKey.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKey.cs index 59e35c4d..f9fccd2d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKey.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKey.cs @@ -1,4 +1,6 @@ -using System; +using Stormpath.SDK.Api; +using Stormpath.SDK.Impl.Utility; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,7 +8,76 @@ namespace Stormpath.SDK.Impl.Api { - internal class ClientApiKey + internal class ClientApiKey : IClientApiKey, IEquatable { + private readonly string _id; + private readonly string _secret; + + private static readonly GenericComparer _comparer = new GenericComparer( + (ClientApiKey x, ClientApiKey y) => + { + return x._id == y._id && x._secret == y._secret; + }, + (ClientApiKey x) => + { + return HashCode.Start + .Hash(x._id); + }); + + + internal ClientApiKey(string id, string secret) + { + if (string.IsNullOrEmpty(id)) + throw new ArgumentNullException("API key ID cannot be null or empty."); + + if (string.IsNullOrEmpty(secret)) + throw new ArgumentNullException("API key secret cannot be null or empty."); + + _id = id; + _secret = secret; + } + + string IClientApiKey.GetId() + { + return _id; + } + + string IClientApiKey.GetSecret() + { + return _secret; + } + + #region Boilerplate comparison and equality stuff + + public bool Equals(ClientApiKey other) + { + return Equals((object)other); + } + + public override bool Equals(object obj) + { + var other = obj as ClientApiKey; + if (obj == null) + return false; + + return _comparer.Equals(this, other); + } + + public override int GetHashCode() + { + return _comparer.GetHashCode(this); + } + + public static bool operator ==(ClientApiKey x, ClientApiKey y) + { + return _comparer.Equals(x, y); + } + + public static bool operator !=(ClientApiKey x, ClientApiKey y) + { + return !(x == y); + } + + #endregion } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKeyBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKeyBuilder.cs deleted file mode 100644 index 7349b4d5..00000000 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKeyBuilder.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Stormpath.SDK.Api; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Stormpath.SDK.Impl.Api -{ - internal sealed class ClientApiKeyBuilder : IApiKeyBuilder - { - private string apiKeyId; - private string apiKeySecret; - - - IApiKey IApiKeyBuilder.Build() - { - throw new NotImplementedException(); - } - - IApiKeyBuilder IApiKeyBuilder.SetId(string id) - { - apiKeyId = id; - return this; - } - - IApiKeyBuilder IApiKeyBuilder.SetSecret(string secret) - { - apiKeySecret = secret; - return this; - } - } -} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultApiKey.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultApiKey.cs deleted file mode 100644 index 87cfc077..00000000 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultApiKey.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Stormpath.SDK.Api; - -namespace Stormpath.SDK.Impl.Api -{ - internal class DefaultApiKey : IApiKey - { - Task IApiKey.GetIdAsync() - { - return Task.FromResult("foo"); - } - - Task IApiKey.GetSecretAsync() - { - return Task.FromResult("bar"); - } - } -} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultClientApiKeyBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultClientApiKeyBuilder.cs new file mode 100644 index 00000000..ba0a5e29 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultClientApiKeyBuilder.cs @@ -0,0 +1,238 @@ +using Stormpath.SDK.Api; +using Stormpath.SDK.Impl.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using System.Configuration; +using Stormpath.SDK.Impl.Utility; + +namespace Stormpath.SDK.Impl.Api +{ + internal sealed class DefaultClientApiKeyBuilder : IClientApiKeyBuilder + { + private static readonly string DefaultIdPropertyName = "apiKey.id"; + private static readonly string DefaultSecretPropertyName = "apiKey.secret"; + + private static readonly string DefaultApiKeyPropertiesFileLocation = + Path.Combine(Environment.ExpandEnvironmentVariables("%HOMEDRIVE%%HOMEPATH%"), ".stormpath\\", "apiKey.properties"); + + private string _apiKeyId; + private string _apiKeySecret; + private Stream _apiKeyFileInputStream; + private string _apiKeyFilePath; + private string _apiKeyIdPropertyName = DefaultIdPropertyName; + private string _apiKeySecretPropertyName = DefaultSecretPropertyName; + + IClientApiKeyBuilder IClientApiKeyBuilder.SetId(string id) + { + _apiKeyId = id; + return this; + } + + IClientApiKeyBuilder IClientApiKeyBuilder.SetSecret(string secret) + { + _apiKeySecret = secret; + return this; + } + + IClientApiKeyBuilder IClientApiKeyBuilder.SetInputStream(Stream stream) + { + _apiKeyFileInputStream = stream; + return this; + } + + IClientApiKeyBuilder IClientApiKeyBuilder.SetFileLocation(string path) + { + _apiKeyFilePath = path; + return this; + } + + IClientApiKeyBuilder IClientApiKeyBuilder.SetIdPropertyName(string idPropertyName) + { + _apiKeyIdPropertyName = idPropertyName; + return this; + } + + IClientApiKeyBuilder IClientApiKeyBuilder.SetSecretPropertyName(string secretPropertyName) + { + _apiKeySecretPropertyName = secretPropertyName; + return this; + } + + + IClientApiKey IClientApiKeyBuilder.Build() + { + // 1. Try to load default API key properties file. Lowest priority + var defaultProperties = GetDefaultApiKeyFileProperties(); + string id = defaultProperties?.GetProperty(_apiKeyIdPropertyName); + string secret = defaultProperties?.GetProperty(_apiKeySecretPropertyName); + + // 2. Try file location specified by environment variables + var envFileLocation = Environment.GetEnvironmentVariable("STORMPATH_API_KEY_FILE"); + if (!string.IsNullOrEmpty(envFileLocation)) + { + var envProperties = GetPropertiesFromEnvironmentVariableFileLocation(envFileLocation); + id = envProperties?.GetProperty(_apiKeyIdPropertyName, defaultValue: id); + secret = envProperties?.GetProperty(_apiKeySecretPropertyName, defaultValue: secret); + } + + // 3. Try environment variables directly + var idFromEnvironment = Environment.GetEnvironmentVariable("STORMPATH_API_KEY_ID"); + var secretFromEnvironment = Environment.GetEnvironmentVariable("STORMPATH_API_KEY_SECRET"); + bool retrievedValuesFromEnvironment = !string.IsNullOrEmpty(idFromEnvironment) && !string.IsNullOrEmpty(secretFromEnvironment); + if (retrievedValuesFromEnvironment) + { + id = idFromEnvironment; + secret = secretFromEnvironment; + } + + // 4. Try file location specified by web.config/app.config + var appConfigFileLocation = ConfigurationManager.AppSettings["STORMPATH_API_KEY_FILE"]; + if (!string.IsNullOrEmpty(appConfigFileLocation)) + { + var appConfigProperties = GetPropertiesFromAppConfigFileLocation(appConfigFileLocation); + id = appConfigProperties?.GetProperty(_apiKeyIdPropertyName, defaultValue: id); + secret = appConfigProperties?.GetProperty(_apiKeySecretPropertyName, defaultValue: secret); + } + + // 5. Try web.config/app.config keys directly + var idFromAppConfig = ConfigurationManager.AppSettings["STORMPATH_API_KEY_ID"]; + var secretFromAppConfig = ConfigurationManager.AppSettings["STORMPATH_API_KEY_SECRET"]; + bool retrievedValuesFromAppConfig = !string.IsNullOrEmpty(idFromAppConfig) && !string.IsNullOrEmpty(secretFromAppConfig); + if (retrievedValuesFromAppConfig) + { + id = idFromAppConfig; + secret = secretFromAppConfig; + } + + // 6. Try configured property file + if (!string.IsNullOrEmpty(_apiKeyFilePath)) + { + var fileProperties = GetPropertiesFromFile(); + id = fileProperties?.GetProperty(_apiKeyIdPropertyName, defaultValue: id); + secret = fileProperties?.GetProperty(_apiKeySecretPropertyName, defaultValue: secret); + } + + // 7. Try an input stream that was passed to us + if (_apiKeyFileInputStream != null) + { + var streamProperties = GetPropertiesFromStream(); + id = streamProperties?.GetProperty(_apiKeyIdPropertyName, defaultValue: id); + secret = streamProperties?.GetProperty(_apiKeySecretPropertyName, defaultValue: secret); + } + + // 8. Explicitly-configured values always take precedence + id = _apiKeyId.OrIfEmptyUse(id); + secret = _apiKeySecret.OrIfEmptyUse(secret); + + if (string.IsNullOrEmpty(id)) + { + var message = "Unable to find an API Key ID, either from explicit configuration (for example, " + + nameof(IClientApiKeyBuilder) + ".setApiKeyId), or from a file location.\r\n" + + "Please provide the API Key ID by one of these methods."; + throw new ApplicationException(message); + } + + if (string.IsNullOrEmpty(secret)) + { + var message = "Unable to find an API Key Secret, either from explicit configuration (for example, " + + nameof(IClientApiKeyBuilder) + ".setApiKeySecret), or from a file location.\r\n" + + "Please provide the API Key Secret by one of these methods."; + throw new ApplicationException(message); + } + + return new ClientApiKey(id, secret); + } + + private Properties GetDefaultApiKeyFileProperties() + { + try + { + var source = File.ReadAllText(DefaultApiKeyPropertiesFileLocation); + return new Properties(source); + } + catch //(Exception ignored) + { + var msg = + $"Unable to find or load default API Key properties file [{DefaultApiKeyPropertiesFileLocation}] " + + "This can safely be ignored as this is a fallback location - other more specific locations will be checked."; + // todo - log + return null; + } + } + + private Properties GetPropertiesFromEnvironmentVariableFileLocation(string path) + { + try + { + var source = File.ReadAllText(path); + var properties = new Properties(source); + return properties; + } + catch //(Exception ignored) + { + var msg = + $"Unable to load API Key properties file [{path}] specified by environment variable " + + "STORMPATH_API_KEY_FILE. This can safely be ignored as this is a fallback location - " + + "other more specific locations will be checked."; + // todo - log + return null; + } + } + + private Properties GetPropertiesFromAppConfigFileLocation(string path) + { + try + { + var source = File.ReadAllText(path); + var properties = new Properties(source); + return properties; + } + catch //(Exception ignored) + { + var msg = + $"Unable to load API Key properties file [{path}] specified by config key " + + "STORMPATH_API_KEY_FILE. This can safely be ignored as this is a fallback location - " + + "other more specific locations will be checked."; + // todo - log + return null; + } + } + + private Properties GetPropertiesFromFile() + { + try + { + var source = File.ReadAllText(_apiKeyFilePath); + var properties = new Properties(source); + return properties; + } + catch (Exception ex) + { + throw new ApplicationException( + $"Unable to read properties from specified file location [{_apiKeyFilePath}]", ex); + } + } + + private Properties GetPropertiesFromStream() + { + if (!_apiKeyFileInputStream.CanRead) return null; + try + { + using (var reader = new StreamReader(_apiKeyFileInputStream)) + { + var source = reader.ReadToEnd(); + return new Properties(source); + } + } + catch (Exception ex) + { + throw new ApplicationException("Unable to read properties from specified input stream.", ex); + } + } + + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs new file mode 100644 index 00000000..53215339 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Extensions +{ + internal static class StringExtensions + { + public static string OrIfEmptyUse(this string source, string defaultValue) + { + if (string.IsNullOrEmpty(source)) return defaultValue; + return source; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs new file mode 100644 index 00000000..dec7fba3 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace Stormpath.SDK.Impl.Utility +{ + public class GenericComparer : IEqualityComparer + { + private readonly Func _areEqualFunc; + private readonly Func _hashFunc; + + public GenericComparer(Func areEqualFunc, Func hashFunc) + { + _areEqualFunc = areEqualFunc; + _hashFunc = hashFunc; + } + + public bool Equals(T x, T y) + { + bool areSameReference = ReferenceEquals(x, y); + if (areSameReference) return true; + + bool eitherIsNull = ReferenceEquals(x, null) || ReferenceEquals(y, null); + if (eitherIsNull) return false; + + var result = false; + return result; + } + + public int GetHashCode(T obj) + { + // Check whether the object is null + bool objectIsNull = ReferenceEquals(obj, null); + if (objectIsNull) return 0; + + return HashCode.Start; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/HashCode.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/HashCode.cs new file mode 100644 index 00000000..4b30029c --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/HashCode.cs @@ -0,0 +1,42 @@ +namespace Stormpath.SDK.Impl.Utility +{ + // Borrowed from Şafak Gür (http://stackoverflow.com/a/18613926/3191599) + // Helper extension that allows hashcodes to be calcualated fluently + public struct HashCode + { + private readonly int hashCode; + + public HashCode(int hashCode) + { + this.hashCode = hashCode; + } + + public static HashCode Start + { + get { return new HashCode(17); } + } + + public static implicit operator int (HashCode hashCode) + { + return hashCode.GetHashCode(); + } + + public HashCode Include(int existingHash) + { + unchecked { existingHash += hashCode * 31; } + return new HashCode(existingHash); + } + + public HashCode Hash(T obj) + { + var h = obj != null ? obj.GetHashCode() : 0; + unchecked { h += hashCode * 31; } + return new HashCode(h); + } + + public override int GetHashCode() + { + return hashCode; + } + } +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Properties.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Properties.cs new file mode 100644 index 00000000..690a89d5 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Properties.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Utility +{ + internal class Properties + { + private static readonly char[] IgnoreLinesStartingWith = { '#', '!' }; + private readonly IDictionary _props; + + public Properties(string input) + { + _props = Parse(input); + } + + public string GetProperty(string key) + { + string value; + if (_props.TryGetValue(key, out value)) return value; + return null; + } + + public string GetProperty(string key, string defaultValue) + { + var value = GetProperty(key); + if (value != null) return value; + return defaultValue; + } + + public int Count() + { + return _props.Count; + } + + private static IDictionary Parse(string input) + { + // TODO Future: support the Java .properties spec better (this will work for now) + + input = input?.Trim(); + if (string.IsNullOrEmpty(input)) return new Dictionary(); + + var goodLines = input + .Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => !(IgnoreLinesStartingWith.Contains(x.First()))); + + var pairs = goodLines + .Select(x => x.Split('=')) + .Where(x => x.Length == 2); + + return pairs.ToDictionary(pair => pair[0].Trim(), pair => pair[1].Trim()); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs index f154f682..738c4c5b 100644 --- a/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs +++ b/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs @@ -34,3 +34,4 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: InternalsVisibleTo("Stormpath.SDK.Tests")] \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index edffd7a5..3c490b14 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -35,6 +35,7 @@ + @@ -44,10 +45,18 @@ - - + + + + + + + + + + From a401141e891ec38484c84c6031ab6b1b65733f45 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 3 Aug 2015 13:03:06 -0700 Subject: [PATCH 012/238] Working ClientApiKeyBuilder and tests --- .../SDK/ClientApiKeysBuilderTests.cs | 300 ++++++++++++------ .../Stormpath.SDK.Tests.csproj | 11 + .../Stormpath.SDK.Tests/packages.config | 5 + .../Stormpath.SDK/Api/ClientApiKeys.cs | 13 +- .../Stormpath.SDK/Api/IClientApiKeyBuilder.cs | 8 +- .../Impl/Api/DefaultClientApiKeyBuilder.cs | 59 ++-- .../Utility/ConfigurationManagerWrapper.cs | 22 ++ .../Impl/Utility/EnvironmentWrapper.cs | 21 ++ .../Stormpath.SDK/Impl/Utility/FileWrapper.cs | 16 + .../Impl/Utility/GenericComparer.cs | 2 +- .../Stormpath.SDK/Impl/Utility/HashCode.cs | 2 +- .../Impl/Utility/IConfigurationManager.cs | 15 + .../Impl/Utility/IEnvironment.cs | 15 + .../Stormpath.SDK/Impl/Utility/IFile.cs | 14 + .../Stormpath.SDK/Properties/AssemblyInfo.cs | 3 +- .../Stormpath.SDK/Stormpath.SDK.csproj | 12 + Stormpath.SDK/Stormpath.SDK/packages.config | 4 + 17 files changed, 383 insertions(+), 139 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/packages.config create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/ConfigurationManagerWrapper.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/EnvironmentWrapper.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/FileWrapper.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/IConfigurationManager.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/IEnvironment.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/IFile.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/packages.config diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/SDK/ClientApiKeysBuilderTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/SDK/ClientApiKeysBuilderTests.cs index c69bb0d1..a7cc4bed 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/SDK/ClientApiKeysBuilderTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/SDK/ClientApiKeysBuilderTests.cs @@ -1,20 +1,17 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.IO; using System.Linq; using Stormpath.SDK.Api; +using Stormpath.SDK.Impl.Api; +using Stormpath.SDK.Impl.Utility; +using NSubstitute; +using Shouldly; namespace Stormpath.SDK.Tests.SDK { [TestClass] public class ClientApiKeysBuilderTests { - public static void DeleteFileIfExists(string location) - { - if (File.Exists(location)) - File.Delete(location); - } - [TestClass] public class Building_With_Missing_Values { @@ -42,189 +39,302 @@ public void With_missing_secret_throws_error() [TestClass] public class Building_With_Default_Properties_File { - // Note that these tests cannot be run in parallel as they attempt to edit the same file. - private readonly string defaultLocation = System.IO.Path.Combine(Environment.ExpandEnvironmentVariables("%HOMEDRIVE%%HOMEPATH%"), ".stormpath\\", "apiKey.properties"); private readonly string fileContents = "apiKey.id = 144JVZINOF5EBNCMG9EXAMPLE\r\n" + "apiKey.secret = lWxOiKqKPNwJmSldbiSkEbkNjgh2uRSNAb+AEXAMPLE"; + private IClientApiKeyBuilder _builder; + private IFile _file; + [TestInitialize] public void Setup() { - if (File.Exists(defaultLocation)) - throw new Exception("You already have a file in the default Stormpath key location."); - - if (!Directory.Exists(Path.GetDirectoryName(defaultLocation))) - Directory.CreateDirectory(Path.GetDirectoryName(defaultLocation)); - - File.WriteAllText(defaultLocation, fileContents); - } + _file = Substitute.For(); + _file.ReadAllText(defaultLocation) + .Returns(fileContents); - [TestCleanup] - public void Teardown() - { - DeleteFileIfExists(defaultLocation); - bool isDirectoryEmpty = !Directory.GetFiles(Path.GetDirectoryName(defaultLocation)).Any(); - if (isDirectoryEmpty) - Directory.Delete(Path.GetDirectoryName(defaultLocation)); + _builder = new DefaultClientApiKeyBuilder(Substitute.For(), Substitute.For(), _file); } [TestMethod] [TestCategory(nameof(ClientApiKeys) + ".Builder")] public void With_default_properties_file() { - var clientApiKey = ClientApiKeys.Builder() - .Build(); + var clientApiKey = _builder.Build(); - Assert.AreEqual("144JVZINOF5EBNCMG9EXAMPLE", clientApiKey.GetId()); - Assert.AreEqual("lWxOiKqKPNwJmSldbiSkEbkNjgh2uRSNAb+AEXAMPLE", clientApiKey.GetSecret()); + clientApiKey.GetId().ShouldBe("144JVZINOF5EBNCMG9EXAMPLE"); + clientApiKey.GetSecret().ShouldBe("lWxOiKqKPNwJmSldbiSkEbkNjgh2uRSNAb+AEXAMPLE"); } [TestMethod] [TestCategory(nameof(ClientApiKeys) + ".Builder")] public void Default_properties_file_is_lower_priority_than_explicit() { - var clientApiKey = ClientApiKeys.Builder() + var clientApiKey = _builder .SetId("different") .SetSecret("also_different") .Build(); - Assert.AreEqual("different", clientApiKey.GetId()); - Assert.AreEqual("also_different", clientApiKey.GetSecret()); + clientApiKey.GetId().ShouldBe("different"); + clientApiKey.GetSecret().ShouldBe("also_different"); } } [TestClass] public class Building_With_Environment_Variable_File { - // Note that these tests cannot be run in parallel because they edit the same environment variable. - private readonly string envVariableName = "STORMPATH_API_KEY_FILE"; private readonly string testLocation = "envfile.properties"; private readonly string fileContents = "apiKey.id = envId\r\n" + "apiKey.secret = envSecret\r\n"; + private IClientApiKeyBuilder _builder; + private IEnvironment _environment; + private IFile _file; + [TestInitialize] public void Setup() { - bool environmentClean = string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envVariableName)); - if (!environmentClean) - throw new Exception($"You already have a value for the environment variable {envVariableName}."); + _environment = Substitute.For(); + _environment.GetEnvironmentVariable(envVariableName) + .Returns(testLocation); - DeleteFileIfExists(testLocation); + _file = Substitute.For(); + _file.ReadAllText(testLocation) + .Returns(fileContents); - File.WriteAllText(testLocation, fileContents); - Environment.SetEnvironmentVariable(envVariableName, testLocation); - } - - [TestCleanup] - public void Teardown() - { - if (Environment.GetEnvironmentVariable(envVariableName) == testLocation) - Environment.SetEnvironmentVariable(envVariableName, null); - - DeleteFileIfExists(testLocation); + _builder = new DefaultClientApiKeyBuilder(Substitute.For(), _environment, _file); } [TestMethod] [TestCategory(nameof(ClientApiKeys) + ".Builder")] public void With_properties_file_from_env_variable() { - var clientApiKey = ClientApiKeys.Builder() - .Build(); + var clientApiKey = _builder.Build(); - Assert.AreEqual("envId", clientApiKey.GetId()); - Assert.AreEqual("envSecret", clientApiKey.GetSecret()); + clientApiKey.GetId().ShouldBe("envId"); + clientApiKey.GetSecret().ShouldBe("envSecret"); } [TestMethod] [TestCategory(nameof(ClientApiKeys) + ".Builder")] public void Env_variable_properties_file_is_lower_priority_than_explicit() { - var clientApiKey = ClientApiKeys.Builder() + var clientApiKey = _builder .SetId("different") .SetSecret("also_different") .Build(); - Assert.AreEqual("different", clientApiKey.GetId()); - Assert.AreEqual("also_different", clientApiKey.GetSecret()); + clientApiKey.GetId().ShouldBe("different"); + clientApiKey.GetSecret().ShouldBe("also_different"); } } [TestClass] public class Building_With_Environment_Variable_Values { - // Note that these tests cannot be run in parallel because they edit the same environment variable. - private readonly string idVariableName = "STORMPATH_API_KEY_ID"; private readonly string fakeApiKeyId = "idSetByEnv"; private readonly string secretVariableName = "STORMPATH_API_KEY_SECRET"; private readonly string fakeApiSecretId = "secretSetByEnv"; + private IClientApiKeyBuilder _builder; + private IEnvironment _environment; + [TestInitialize] public void Setup() { - bool environmentClean = - string.IsNullOrEmpty(Environment.GetEnvironmentVariable(idVariableName)) && - string.IsNullOrEmpty(Environment.GetEnvironmentVariable(secretVariableName)); - if (!environmentClean) - throw new Exception($"You already have a value for the environment variable {idVariableName} or {secretVariableName}."); - - Environment.SetEnvironmentVariable(idVariableName, fakeApiKeyId); - Environment.SetEnvironmentVariable(secretVariableName, fakeApiSecretId); + _environment = Substitute.For(); + _environment.GetEnvironmentVariable(idVariableName) + .Returns(fakeApiKeyId); + _environment.GetEnvironmentVariable(secretVariableName) + .Returns(fakeApiSecretId); + + _builder = new DefaultClientApiKeyBuilder(Substitute.For(), _environment, Substitute.For()); } - [TestCleanup] - public void Teardown() + [TestMethod] + [TestCategory(nameof(ClientApiKeys) + ".Builder")] + public void With_values_from_env_variables() { - if (Environment.GetEnvironmentVariable(idVariableName) == fakeApiKeyId) - Environment.SetEnvironmentVariable(idVariableName, null); - if (Environment.GetEnvironmentVariable(secretVariableName) == fakeApiSecretId) - Environment.SetEnvironmentVariable(secretVariableName, null); + var clientApiKey = _builder + .Build(); + + clientApiKey.GetId().ShouldBe(fakeApiKeyId); + clientApiKey.GetSecret().ShouldBe(fakeApiSecretId); } [TestMethod] [TestCategory(nameof(ClientApiKeys) + ".Builder")] - public void With_values_from_env_variables() + public void Env_variable_values_are_lower_priority_than_explicit() { - var clientApiKey = ClientApiKeys.Builder() + var clientApiKey = _builder + .SetId("different") + .SetSecret("also_different") .Build(); - Assert.AreEqual(fakeApiKeyId, clientApiKey.GetId()); - Assert.AreEqual(fakeApiSecretId, clientApiKey.GetSecret()); + clientApiKey.GetId().ShouldBe("different"); + clientApiKey.GetSecret().ShouldBe("also_different"); + } + } + + [TestClass] + public class Building_With_AppConfig_File + { + private readonly string configVariableName = "STORMPATH_API_KEY_FILE"; + private readonly string testLocation = "appConfig.properties"; + private readonly string fileContents = + "apiKey.id = fromAppConfig?\r\n" + + "apiKey.secret = fooSecret!\r\n"; + + private IClientApiKeyBuilder _builder; + private IConfigurationManager _config; + private IFile _file; + + [TestInitialize] + public void Setup() + { + _config = Substitute.For(); + _config.AppSettings.Returns(new System.Collections.Specialized.NameValueCollection() + { + { configVariableName, testLocation } + }); + + _file = Substitute.For(); + _file.ReadAllText(testLocation) + .Returns(fileContents); + + _builder = new DefaultClientApiKeyBuilder(_config, Substitute.For(), _file); } [TestMethod] [TestCategory(nameof(ClientApiKeys) + ".Builder")] - public void Env_variable_values_is_lower_priority_than_explicit() + public void With_properties_file_from_app_config() { - var clientApiKey = ClientApiKeys.Builder() + var clientApiKey = _builder.Build(); + + clientApiKey.GetId().ShouldBe("fromAppConfig?"); + clientApiKey.GetSecret().ShouldBe("fooSecret!"); + } + + [TestMethod] + [TestCategory(nameof(ClientApiKeys) + ".Builder")] + public void App_config_properties_file_is_lower_priority_than_explicit() + { + var clientApiKey = _builder .SetId("different") .SetSecret("also_different") .Build(); - Assert.AreEqual("different", clientApiKey.GetId()); - Assert.AreEqual("also_different", clientApiKey.GetSecret()); + clientApiKey.GetId().ShouldBe("different"); + clientApiKey.GetSecret().ShouldBe("also_different"); + } + } + + [TestClass] + public class Building_With_AppConfig_Values + { + private readonly string idVariableName = "STORMPATH_API_KEY_ID"; + private readonly string fakeApiKeyId = "idSetByAppConfig"; + private readonly string secretVariableName = "STORMPATH_API_KEY_SECRET"; + private readonly string fakeApiSecretId = "secretSetByAppConfig"; + + private IClientApiKeyBuilder _builder; + private IConfigurationManager _config; + + [TestInitialize] + public void Setup() + { + _config = Substitute.For(); + _config.AppSettings.Returns(new System.Collections.Specialized.NameValueCollection() + { + { idVariableName, fakeApiKeyId}, + { secretVariableName, fakeApiSecretId } + }); + + _builder = new DefaultClientApiKeyBuilder(_config, Substitute.For(), Substitute.For()); + } + + [TestMethod] + [TestCategory(nameof(ClientApiKeys) + ".Builder")] + public void With_properties_file_from_app_config() + { + var clientApiKey = _builder.Build(); + + clientApiKey.GetId().ShouldBe(fakeApiKeyId); + clientApiKey.GetSecret().ShouldBe(fakeApiSecretId); + } + + [TestMethod] + [TestCategory(nameof(ClientApiKeys) + ".Builder")] + public void App_config_values_are_lower_priority_than_explicit() + { + var clientApiKey = _builder + .SetId("different") + .SetSecret("also_different") + .Build(); + + clientApiKey.GetId().ShouldBe("different"); + clientApiKey.GetSecret().ShouldBe("also_different"); + } + } + + [TestClass] + public class Building_With_Stream + { + private IClientApiKeyBuilder _builder; + + [TestInitialize] + public void Setup() + { + _builder = new DefaultClientApiKeyBuilder(Substitute.For(), Substitute.For(), Substitute.For()); + } + + [TestMethod] + [TestCategory(nameof(ClientApiKeys) + ".Builder")] + public void With_values_from_stream() + { + var streamContents = + "apiKey.id = streams_r_neet\r\n" + + "apiKey.secret = pls_dont_steal!\r\n"; + + using (var stream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(streamContents))) + { + var clientApiKey = _builder + .SetInputStream(stream) + .Build(); + + clientApiKey.GetId().ShouldBe("streams_r_neet"); + clientApiKey.GetSecret().ShouldBe("pls_dont_steal!"); + } } } [TestClass] public class Building_With_Explicit_Values { + private IClientApiKeyBuilder _builder; + + [TestInitialize] + public void Setup() + { + _builder = new DefaultClientApiKeyBuilder(Substitute.For(), Substitute.For(), Substitute.For()); + } + [TestMethod] [TestCategory(nameof(ClientApiKeys) + ".Builder")] public void With_explicit_values() { - var clientApiKey = ClientApiKeys.Builder() + var clientApiKey = _builder .SetId("foo") .SetSecret("bar") .Build(); - Assert.AreEqual("foo", clientApiKey.GetId()); - Assert.AreEqual("bar", clientApiKey.GetSecret()); + clientApiKey.GetId().ShouldBe("foo"); + clientApiKey.GetSecret().ShouldBe("bar"); } } @@ -236,43 +346,43 @@ public class Building_With_Properties_File "apiKey.id = foobar\r\n" + "apiKey.secret = bazsecret!\r\n"; + private IClientApiKeyBuilder _builder; + private IFile _file; + [TestInitialize] public void Setup() { - DeleteFileIfExists(testLocation); - File.WriteAllText(testLocation, fileContents); - } + _file = Substitute.For(); + _file.ReadAllText(testLocation) + .Returns(fileContents); - [TestCleanup] - public void Teardown() - { - DeleteFileIfExists(testLocation); + _builder = new DefaultClientApiKeyBuilder(Substitute.For(), Substitute.For(), _file); } [TestMethod] [TestCategory(nameof(ClientApiKeys) + ".Builder")] public void With_properties_file() { - var clientApiKey = ClientApiKeys.Builder() + var clientApiKey = _builder .SetFileLocation(testLocation) .Build(); - Assert.AreEqual("foobar", clientApiKey.GetId()); - Assert.AreEqual("bazsecret!", clientApiKey.GetSecret()); + clientApiKey.GetId().ShouldBe("foobar"); + clientApiKey.GetSecret().ShouldBe("bazsecret!"); } [TestMethod] [TestCategory(nameof(ClientApiKeys) + ".Builder")] public void Properties_file_is_lower_priority_than_explicit() { - var clientApiKey = ClientApiKeys.Builder() + var clientApiKey = _builder .SetFileLocation(testLocation) .SetId("different") .SetSecret("also_different") .Build(); - Assert.AreEqual("different", clientApiKey.GetId()); - Assert.AreEqual("also_different", clientApiKey.GetSecret()); + clientApiKey.GetId().ShouldBe("different"); + clientApiKey.GetSecret().ShouldBe("also_different"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 68ea848a..56237360 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -35,6 +35,14 @@ 4 + + ..\packages\NSubstitute.1.8.2.0\lib\net45\NSubstitute.dll + True + + + ..\packages\Shouldly.2.5.0\lib\net40\Shouldly.dll + True + @@ -66,6 +74,9 @@ + + + diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/packages.config b/Stormpath.SDK/Stormpath.SDK.Tests/packages.config new file mode 100644 index 00000000..39647000 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Api/ClientApiKeys.cs b/Stormpath.SDK/Stormpath.SDK/Api/ClientApiKeys.cs index a4edfc30..6ee4f7b6 100644 --- a/Stormpath.SDK/Stormpath.SDK/Api/ClientApiKeys.cs +++ b/Stormpath.SDK/Stormpath.SDK/Api/ClientApiKeys.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Stormpath.SDK.Api +namespace Stormpath.SDK.Api { public sealed class ClientApiKeys { public static IClientApiKeyBuilder Builder() { - return new Impl.Api.DefaultClientApiKeyBuilder(); + return new Impl.Api.DefaultClientApiKeyBuilder( + new Impl.Utility.ConfigurationManagerWrapper(), + new Impl.Utility.EnvironmentWrapper(), + new Impl.Utility.FileWrapper()); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKeyBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKeyBuilder.cs index fc510db1..edc9b49d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKeyBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Api/IClientApiKeyBuilder.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Stormpath.SDK.Api +namespace Stormpath.SDK.Api { public interface IClientApiKeyBuilder { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultClientApiKeyBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultClientApiKeyBuilder.cs index ba0a5e29..14d3e6d6 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultClientApiKeyBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Api/DefaultClientApiKeyBuilder.cs @@ -1,31 +1,38 @@ -using Stormpath.SDK.Api; -using Stormpath.SDK.Impl.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.IO; -using System.Configuration; -using Stormpath.SDK.Impl.Utility; - -namespace Stormpath.SDK.Impl.Api +namespace Stormpath.SDK.Impl.Api { + using System; + using Extensions; + using SDK.Api; + using Utility; + internal sealed class DefaultClientApiKeyBuilder : IClientApiKeyBuilder { private static readonly string DefaultIdPropertyName = "apiKey.id"; private static readonly string DefaultSecretPropertyName = "apiKey.secret"; private static readonly string DefaultApiKeyPropertiesFileLocation = - Path.Combine(Environment.ExpandEnvironmentVariables("%HOMEDRIVE%%HOMEPATH%"), ".stormpath\\", "apiKey.properties"); + System.IO.Path.Combine(Environment.ExpandEnvironmentVariables("%HOMEDRIVE%%HOMEPATH%"), ".stormpath\\", "apiKey.properties"); + // Instance fields private string _apiKeyId; private string _apiKeySecret; - private Stream _apiKeyFileInputStream; + private System.IO.Stream _apiKeyFileInputStream; private string _apiKeyFilePath; private string _apiKeyIdPropertyName = DefaultIdPropertyName; private string _apiKeySecretPropertyName = DefaultSecretPropertyName; + // Wrappers for static .NET Framework calls (for easier unit testing) + private readonly IConfigurationManager _configuration; + private readonly IEnvironment _environment; + private readonly IFile _file; + + public DefaultClientApiKeyBuilder(IConfigurationManager configuration, IEnvironment environment, IFile file) + { + _configuration = configuration; + _environment = environment; + _file = file; + } + IClientApiKeyBuilder IClientApiKeyBuilder.SetId(string id) { _apiKeyId = id; @@ -38,7 +45,7 @@ IClientApiKeyBuilder IClientApiKeyBuilder.SetSecret(string secret) return this; } - IClientApiKeyBuilder IClientApiKeyBuilder.SetInputStream(Stream stream) + IClientApiKeyBuilder IClientApiKeyBuilder.SetInputStream(System.IO.Stream stream) { _apiKeyFileInputStream = stream; return this; @@ -71,7 +78,7 @@ IClientApiKey IClientApiKeyBuilder.Build() string secret = defaultProperties?.GetProperty(_apiKeySecretPropertyName); // 2. Try file location specified by environment variables - var envFileLocation = Environment.GetEnvironmentVariable("STORMPATH_API_KEY_FILE"); + var envFileLocation = _environment.GetEnvironmentVariable("STORMPATH_API_KEY_FILE"); if (!string.IsNullOrEmpty(envFileLocation)) { var envProperties = GetPropertiesFromEnvironmentVariableFileLocation(envFileLocation); @@ -80,8 +87,8 @@ IClientApiKey IClientApiKeyBuilder.Build() } // 3. Try environment variables directly - var idFromEnvironment = Environment.GetEnvironmentVariable("STORMPATH_API_KEY_ID"); - var secretFromEnvironment = Environment.GetEnvironmentVariable("STORMPATH_API_KEY_SECRET"); + var idFromEnvironment = _environment.GetEnvironmentVariable("STORMPATH_API_KEY_ID"); + var secretFromEnvironment = _environment.GetEnvironmentVariable("STORMPATH_API_KEY_SECRET"); bool retrievedValuesFromEnvironment = !string.IsNullOrEmpty(idFromEnvironment) && !string.IsNullOrEmpty(secretFromEnvironment); if (retrievedValuesFromEnvironment) { @@ -90,7 +97,7 @@ IClientApiKey IClientApiKeyBuilder.Build() } // 4. Try file location specified by web.config/app.config - var appConfigFileLocation = ConfigurationManager.AppSettings["STORMPATH_API_KEY_FILE"]; + var appConfigFileLocation = _configuration.AppSettings?["STORMPATH_API_KEY_FILE"]; if (!string.IsNullOrEmpty(appConfigFileLocation)) { var appConfigProperties = GetPropertiesFromAppConfigFileLocation(appConfigFileLocation); @@ -99,8 +106,8 @@ IClientApiKey IClientApiKeyBuilder.Build() } // 5. Try web.config/app.config keys directly - var idFromAppConfig = ConfigurationManager.AppSettings["STORMPATH_API_KEY_ID"]; - var secretFromAppConfig = ConfigurationManager.AppSettings["STORMPATH_API_KEY_SECRET"]; + var idFromAppConfig = _configuration.AppSettings?["STORMPATH_API_KEY_ID"]; + var secretFromAppConfig = _configuration.AppSettings?["STORMPATH_API_KEY_SECRET"]; bool retrievedValuesFromAppConfig = !string.IsNullOrEmpty(idFromAppConfig) && !string.IsNullOrEmpty(secretFromAppConfig); if (retrievedValuesFromAppConfig) { @@ -151,7 +158,7 @@ private Properties GetDefaultApiKeyFileProperties() { try { - var source = File.ReadAllText(DefaultApiKeyPropertiesFileLocation); + var source = _file.ReadAllText(DefaultApiKeyPropertiesFileLocation); return new Properties(source); } catch //(Exception ignored) @@ -168,7 +175,7 @@ private Properties GetPropertiesFromEnvironmentVariableFileLocation(string path) { try { - var source = File.ReadAllText(path); + var source = _file.ReadAllText(path); var properties = new Properties(source); return properties; } @@ -187,7 +194,7 @@ private Properties GetPropertiesFromAppConfigFileLocation(string path) { try { - var source = File.ReadAllText(path); + var source = _file.ReadAllText(path); var properties = new Properties(source); return properties; } @@ -206,7 +213,7 @@ private Properties GetPropertiesFromFile() { try { - var source = File.ReadAllText(_apiKeyFilePath); + var source = _file.ReadAllText(_apiKeyFilePath); var properties = new Properties(source); return properties; } @@ -222,7 +229,7 @@ private Properties GetPropertiesFromStream() if (!_apiKeyFileInputStream.CanRead) return null; try { - using (var reader = new StreamReader(_apiKeyFileInputStream)) + using (var reader = new System.IO.StreamReader(_apiKeyFileInputStream)) { var source = reader.ReadToEnd(); return new Properties(source); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/ConfigurationManagerWrapper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/ConfigurationManagerWrapper.cs new file mode 100644 index 00000000..9e824e87 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/ConfigurationManagerWrapper.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Configuration; +using System.Collections.Specialized; + +namespace Stormpath.SDK.Impl.Utility +{ + // A simple wrapper around ConfigurationManager, used for injecting an otherwise sadly static object + internal class ConfigurationManagerWrapper : IConfigurationManager + { + public NameValueCollection AppSettings + { + get + { + return ConfigurationManager.AppSettings; + } + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/EnvironmentWrapper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/EnvironmentWrapper.cs new file mode 100644 index 00000000..a16b04b0 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/EnvironmentWrapper.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Utility +{ + internal class EnvironmentWrapper : IEnvironment + { + public string ExpandEnvironmentVariables(string name) + { + return Environment.ExpandEnvironmentVariables(name); + } + + public string GetEnvironmentVariable(string variable) + { + return Environment.GetEnvironmentVariable(variable); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/FileWrapper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/FileWrapper.cs new file mode 100644 index 00000000..78fcc5ed --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/FileWrapper.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Utility +{ + internal class FileWrapper : IFile + { + public string ReadAllText(string path) + { + return System.IO.File.ReadAllText(path); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs index dec7fba3..d15eea97 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs @@ -3,7 +3,7 @@ namespace Stormpath.SDK.Impl.Utility { - public class GenericComparer : IEqualityComparer + internal class GenericComparer : IEqualityComparer { private readonly Func _areEqualFunc; private readonly Func _hashFunc; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/HashCode.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/HashCode.cs index 4b30029c..cbf4e3ea 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/HashCode.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/HashCode.cs @@ -2,7 +2,7 @@ { // Borrowed from Şafak Gür (http://stackoverflow.com/a/18613926/3191599) // Helper extension that allows hashcodes to be calcualated fluently - public struct HashCode + internal struct HashCode { private readonly int hashCode; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IConfigurationManager.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IConfigurationManager.cs new file mode 100644 index 00000000..053dd2cc --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IConfigurationManager.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Utility +{ + // A simple subset of ConfigurationManager for unit testing + internal interface IConfigurationManager + { + NameValueCollection AppSettings { get; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IEnvironment.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IEnvironment.cs new file mode 100644 index 00000000..01838630 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IEnvironment.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Utility +{ + // A subset of methods available on the static Environment object (used for testing) + internal interface IEnvironment + { + string ExpandEnvironmentVariables(string name); + string GetEnvironmentVariable(string variable); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IFile.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IFile.cs new file mode 100644 index 00000000..20575e6d --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IFile.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Utility +{ + // A subset of methods on System.IO.File, for easy unit testing + internal interface IFile + { + string ReadAllText(string path); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs index 738c4c5b..f563eb04 100644 --- a/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs +++ b/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs @@ -34,4 +34,5 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: InternalsVisibleTo("Stormpath.SDK.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Stormpath.SDK.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 3c490b14..7503f186 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -54,11 +54,23 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.sln b/Stormpath.SDK/Stormpath.SDK.sln index 8a8efa14..4e05f367 100644 --- a/Stormpath.SDK/Stormpath.SDK.sln +++ b/Stormpath.SDK/Stormpath.SDK.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK", "Stormpath. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.Tests", "Stormpath.SDK.Tests\Stormpath.SDK.Tests.csproj", "{3579A409-6C66-4181-8268-C851878E5E22}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.Demo", "Stormpath.Demo\Stormpath.Demo.csproj", "{301DA33A-8F36-47C7-BCD1-B89725689B4C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {3579A409-6C66-4181-8268-C851878E5E22}.Debug|Any CPU.Build.0 = Debug|Any CPU {3579A409-6C66-4181-8268-C851878E5E22}.Release|Any CPU.ActiveCfg = Release|Any CPU {3579A409-6C66-4181-8268-C851878E5E22}.Release|Any CPU.Build.0 = Release|Any CPU + {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From fbf995cea5271dd6b28954e60f3f590ab37722c6 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Tue, 4 Aug 2015 08:47:43 -0700 Subject: [PATCH 021/238] Added todo code to demo --- Stormpath.SDK/Stormpath.Demo/App.config | 9 +- Stormpath.SDK/Stormpath.Demo/Config.cs | 12 ++ Stormpath.SDK/Stormpath.Demo/Program.cs | 107 +++++++++++++++++- .../Stormpath.Demo/Stormpath.Demo.csproj | 5 + .../Stormpath.Demo/demoKeys.properties | 3 + .../Client/AuthenticationScheme.cs | 2 +- Stormpath.SDK/Stormpath.SDK/Client/Clients.cs | 27 +++++ Stormpath.SDK/Stormpath.SDK/Client/IClient.cs | 3 + .../Impl/Client/DefaultClient.cs | 5 + .../{Impl/Utility => Shared}/Enumeration.cs | 2 +- .../Stormpath.SDK/Stormpath.SDK.csproj | 3 +- 11 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.Demo/Config.cs create mode 100644 Stormpath.SDK/Stormpath.Demo/demoKeys.properties create mode 100644 Stormpath.SDK/Stormpath.SDK/Client/Clients.cs rename Stormpath.SDK/Stormpath.SDK/{Impl/Utility => Shared}/Enumeration.cs (99%) diff --git a/Stormpath.SDK/Stormpath.Demo/App.config b/Stormpath.SDK/Stormpath.Demo/App.config index 88fa4027..a097d20b 100644 --- a/Stormpath.SDK/Stormpath.Demo/App.config +++ b/Stormpath.SDK/Stormpath.Demo/App.config @@ -1,6 +1,9 @@  - - - + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.Demo/Config.cs b/Stormpath.SDK/Stormpath.Demo/Config.cs new file mode 100644 index 00000000..e0242900 --- /dev/null +++ b/Stormpath.SDK/Stormpath.Demo/Config.cs @@ -0,0 +1,12 @@ +using System.Configuration; + +namespace Stormpath.Demo +{ + internal static class Config + { + public static string ApiKeyFileLocation + { + get { return ConfigurationManager.AppSettings["ApiKeyFileLocation"]; } + } + } +} diff --git a/Stormpath.SDK/Stormpath.Demo/Program.cs b/Stormpath.SDK/Stormpath.Demo/Program.cs index 15cdf358..a21961f4 100644 --- a/Stormpath.SDK/Stormpath.Demo/Program.cs +++ b/Stormpath.SDK/Stormpath.Demo/Program.cs @@ -1,22 +1,119 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Stormpath.SDK.Api; +using Stormpath.SDK.Client; +using System.Threading; namespace Stormpath.Demo { class Program { static void Main(string[] args) + { + bool isConfigurationPresent = !string.IsNullOrEmpty(Config.ApiKeyFileLocation); + if (!isConfigurationPresent) throw new ArgumentNullException("Please add an ApiKeyFileLocation item to app.config!"); + + // Wire up the console cancel event (Ctrl+C) to cancel async tasks + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (source, evt) => + { + evt.Cancel = true; + cts.Cancel(); + }; + + MainAsync(cts.Token).GetAwaiter().GetResult(); + // Logically equivalent to MainAsync(...).Wait() but allows exceptions to bubble up unwrapped + } + + static async Task MainAsync(CancellationToken ct) { var apiKey = ClientApiKeys.Builder() - .SetId("myID") - .SetSecret("secret") + .SetFileLocation(Config.ApiKeyFileLocation) .Build(); + if (!apiKey.IsValid()) throw new ArgumentException("The provided API key is not valid."); + if (apiKey.GetId() == "") throw new ArgumentException("Please insert your real Stormpath API key ID."); + if (apiKey.GetSecret() == "") throw new ArgumentException("Please insert your real Stormpath API key secret."); + + // Create an IClient object. Everything starts here! + var client = Clients.Builder() + .SetApiKey(apiKey) + .Build(); + // Get current tenant + var tenant = await client.GetCurrentTenantAsync(); + Console.WriteLine($"Current tenant is: {await tenant.GetNameAsync()}"); + + // List applications + // TODO + //Console.WriteLine("\nTenant applications:"); + //var applications = await tenant.GetApplicationsAsync(); + //foreach (var app in applications) + // Console.WriteLine("{0}\t{1}", app.Name, app.Status ? "enabled" : "disabled"); + //var myApp = applications.First(); + + //// Add some users + //Console.WriteLine("\nAdding users to '{0}'...", myApp.Name); + //var addedUsers = new List(); + //var result = await myApp.CreateAccountAsync("tk421@stormpath.com", "Joe", "Stormtrooper", "tk421", "Changeme1"); + //if (result.Status == AccountStatus.Enabled) + // addedUsers.Add(result.Email); + + //result = await myApp.CreateAccountAsync("lando@bespin.co", "Lando", "Calrissian", "lcalrissian", "Changeme1", new + //{ + // quote = "You got a lotta nerve showin' your face around here, after what you pulled." + //}); + //if (result.Status == AccountStatus.Enabled) + // addedUsers.Add(result.Email); + + //// Print them + //Console.WriteLine("\nApplication accounts:"); + //var accounts = await myApp.GetAccountsAsync(); + //foreach (var account in accounts) + //{ + // Console.WriteLine("{0} {1} - {2}", account.Email, account.FullName, account.Status.ToString()); + //} + + //// Authenticate a user + //var loginAs = accounts.First(); + //Console.WriteLine("\nLogging in as {0}...", loginAs.UserName); + //var didLogin = await myApp.AuthenticateAccountAsync(loginAs.UserName, "Changeme1"); + //Console.WriteLine("{0}", didLogin ? "Success!" : "Error :("); + + //// Create a group + //// todo + + //// Assign user to group + //// todo + + //// Clean up! + //Console.WriteLine("\nCleaning up!"); + //Console.WriteLine("Deleting accounts:"); + //foreach (var email in addedUsers) + //{ + // var account = accounts + // .Where(x => x.Email == email) + // .Single(); + // var deleted = await account.DeleteAsync(); + // Console.WriteLine("Deleted {0} - {1}", account.Email, deleted ? "done" : "error"); + //} + + //Console.Write("\nPress any key to exit..."); + //Console.ReadKey(false); + } + + // TODO + static Task SpacebarToContinue(CancellationToken cancelToken) + { + do + { + if (cancelToken.IsCancellationRequested) return Task.FromResult(false); + + var key = Console.ReadKey(true); + + if (key.KeyChar == ' ') return Task.FromResult(true); + + } while (true); } } } diff --git a/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj b/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj index de1d844d..8627d357 100644 --- a/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj +++ b/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj @@ -34,6 +34,7 @@ + @@ -43,11 +44,15 @@ + + + Always + diff --git a/Stormpath.SDK/Stormpath.Demo/demoKeys.properties b/Stormpath.SDK/Stormpath.Demo/demoKeys.properties new file mode 100644 index 00000000..94c1ace3 --- /dev/null +++ b/Stormpath.SDK/Stormpath.Demo/demoKeys.properties @@ -0,0 +1,3 @@ +# Replace these values with your own Stormpath key and secret +apiKey.id = +apiKey.secret = \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Client/AuthenticationScheme.cs b/Stormpath.SDK/Stormpath.SDK/Client/AuthenticationScheme.cs index aed8146e..5189f3ce 100644 --- a/Stormpath.SDK/Stormpath.SDK/Client/AuthenticationScheme.cs +++ b/Stormpath.SDK/Stormpath.SDK/Client/AuthenticationScheme.cs @@ -17,7 +17,7 @@ using System; using Stormpath.SDK.Impl.Http.Authentication; -using Stormpath.SDK.Impl.Utility; +using Stormpath.SDK.Shared; namespace Stormpath.SDK.Client { diff --git a/Stormpath.SDK/Stormpath.SDK/Client/Clients.cs b/Stormpath.SDK/Stormpath.SDK/Client/Clients.cs new file mode 100644 index 00000000..c63717fa --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Client/Clients.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Stormpath.SDK.Client +{ + public sealed class Clients + { + public static IClientBuilder Builder() + { + return new Impl.Client.DefaultClientBuilder(); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs b/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs index 11f65709..81f8b88a 100644 --- a/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System.Threading.Tasks; using Stormpath.SDK.Tenant; namespace Stormpath.SDK.Client @@ -26,5 +27,7 @@ public interface IClient : ITenantActions AuthenticationScheme AuthenticationScheme { get; } int ConnectionTimeout { get; } + + Task GetCurrentTenantAsync(); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs index dba499b3..c3001d76 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs @@ -95,5 +95,10 @@ Task ITenantActions.VerifyAccountEmailAsync() { throw new NotImplementedException(); } + + Task IClient.GetCurrentTenantAsync() + { + throw new NotImplementedException(); + } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Enumeration.cs b/Stormpath.SDK/Stormpath.SDK/Shared/Enumeration.cs similarity index 99% rename from Stormpath.SDK/Stormpath.SDK/Impl/Utility/Enumeration.cs rename to Stormpath.SDK/Stormpath.SDK/Shared/Enumeration.cs index 78392c69..cb2de71a 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Enumeration.cs +++ b/Stormpath.SDK/Stormpath.SDK/Shared/Enumeration.cs @@ -20,7 +20,7 @@ using System.Linq; using System.Reflection; -namespace Stormpath.SDK.Impl.Utility +namespace Stormpath.SDK.Shared { // Borrowed this excellent type-safe enum pattern from J. Bogard // https://lostechies.com/jimmybogard/2008/08/12/enumeration-classes/ diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 93bd28dd..69f17c9d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -55,6 +55,7 @@ + @@ -72,7 +73,7 @@ - + From d3fd6041e257c49f0642972f159f6d438d6d3f38 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Sun, 9 Aug 2015 13:30:18 -0700 Subject: [PATCH 022/238] ISO 8601 helper class + tests --- .../Impl/Utility/Iso8601Tests.cs | 54 +++++++++++++++++++ .../Stormpath.SDK.Tests.csproj | 1 + .../Stormpath.SDK/Impl/Utility/Iso8601.cs | 30 +++++++++++ .../Stormpath.SDK/Stormpath.SDK.csproj | 1 + 4 files changed, 86 insertions(+) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601Tests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601Tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601Tests.cs new file mode 100644 index 00000000..0f6dbaea --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601Tests.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stormpath.SDK.Impl.Utility; + +namespace Stormpath.SDK.Tests.Impl.Utility +{ + [TestClass] + public class Iso8601Tests + { + [TestMethod] + public void Format_from_local_date() + { + // Midnight Pacific Time, Jan 1, 2015 = 2015-01-01 08:00 UTC + var midnightPacificTimeJan1 = new DateTimeOffset(2015, 01, 01, 00, 00, 00, 00, TimeSpan.FromHours(-8)); + var eightHoursLaterUtc8601 = Iso8601.Format(midnightPacificTimeJan1); + eightHoursLaterUtc8601.ShouldBe("2015-01-01T08:00:00.000Z"); + + // 4PM Pacific Time, Dec 31 2015 = 2015-01-01 00:00 UTC + var fourInTheLocalAfternoonDec31 = new DateTime(2014, 12, 31, 16, 00, 00); + var midnightUtcIso8601 = Iso8601.Format(new DateTimeOffset(fourInTheLocalAfternoonDec31, TimeZone.CurrentTimeZone.GetUtcOffset(fourInTheLocalAfternoonDec31))); + midnightUtcIso8601.ShouldBe("2015-01-01T00:00:00.000Z"); + } + + [TestMethod] + public void Format_from_UTC_date() + { + var alreadyInUtc = new DateTimeOffset(2016, 02, 01, 12, 00, 00, TimeSpan.Zero); + var directlyInto8601 = Iso8601.Format(alreadyInUtc); + directlyInto8601.ShouldBe("2016-02-01T12:00:00.000Z"); + + var dateTimeInUtc = new DateTime(2016, 02, 02, 16, 30, 00, DateTimeKind.Utc); + var alsoConvertsFine = Iso8601.Format(new DateTimeOffset(dateTimeInUtc, TimeSpan.Zero)); + alsoConvertsFine.ShouldBe("2016-02-02T16:30:00.000Z"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index f2e00341..b9f5923e 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -65,6 +65,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs new file mode 100644 index 00000000..fa8e5caa --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Stormpath.SDK.Impl.Utility +{ + public static class Iso8601 + { + public static string Format(DateTimeOffset dto) + { + return dto.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", + System.Globalization.CultureInfo.InvariantCulture); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 69f17c9d..44fe2e78 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -73,6 +73,7 @@ + From 2cc345b8d9d8b3aeb22b0ed296c0129c5dd94159 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Sun, 9 Aug 2015 16:56:47 -0700 Subject: [PATCH 023/238] First pass at LINQ provider --- .../CollectionResourceRequestModelTests.cs | 87 +++++++ .../Impl/Linq/LinqTests.cs | 177 ++++++++++++++ .../Impl/Linq/TestHarness.cs | 50 ++++ .../Stormpath.SDK.Tests.csproj | 7 + .../Stormpath.SDK/Account/AccountStatus.cs | 37 +++ .../Stormpath.SDK/Account/IAccount.cs | 25 +- .../Stormpath.SDK/Account/IAccountList.cs | 2 +- .../Application/IApplicationList.cs | 2 +- .../Stormpath.SDK/DataStore/IDataStore.cs | 30 +++ .../Stormpath.SDK/Directory/IDirectoryList.cs | 2 +- .../Stormpath.SDK/Group/IGroupList.cs | 2 +- .../Impl/Client/DefaultClientBuilder.cs | 1 - .../{DS => DataStore}/IInternalDataStore.cs | 2 +- .../Linq/CollectionResourceQueryExecutor.cs | 60 +++++ .../CollectionResourceQueryModelVisitor.cs | 191 +++++++++++++++ .../CollectionResourceRequestModelCompiler.cs | 222 ++++++++++++++++++ ...ollectionResourceWhereExpressionVisitor.cs | 194 +++++++++++++++ .../Impl/Linq/Parsing/ExpandExpressionNode.cs | 60 +++++ .../Impl/Linq/Parsing/ExpandResultOperator.cs | 62 +++++ .../Impl/Linq/Parsing/ExtendedQueryParser.cs | 42 ++++ .../Impl/Linq/Parsing/FilterClause.cs | 46 ++++ .../Impl/Linq/Parsing/FilterExpressionNode.cs | 55 +++++ .../AbstractAttributeTermModel.cs | 24 ++ .../CollectionResourceRequestModel.cs | 81 +++++++ .../DatetimeAttributeTermModel.cs | 54 +++++ .../Impl/Linq/RequestModel/ExpansionTerm.cs | 46 ++++ .../Impl/Linq/RequestModel/OrderByModel.cs | 32 +++ .../StringAttributeMatchingType.cs | 42 ++++ .../RequestModel/StringAttributeTermModel.cs | 33 +++ .../WhereAttributeTermInProgressModel.cs | 55 +++++ .../CollectionLinkMethodNameTranslator.cs | 34 +++ .../DatetimeFieldNameTranslator.cs | 35 +++ .../FieldNameTranslator.cs | 38 +++ .../LinkMethodNameTranslator.cs | 35 +++ .../StringHelperMethodNameTranslator.cs | 39 +++ .../Resource/CollectionResourceQueryable.cs | 81 +++++++ .../CollectionResourceQueryableExtensions.cs | 64 +++++ .../Stormpath.SDK/Resource/IAuditable.cs | 28 +++ ...rce.cs => ICollectionResourceQueryable.cs} | 6 +- .../Stormpath.SDK/Resource/ICustomData.cs | 25 ++ .../Stormpath.SDK/Resource/IExtendable.cs | 26 ++ .../Stormpath.SDK/Stormpath.SDK.csproj | 37 ++- Stormpath.SDK/Stormpath.SDK/packages.config | 1 + 43 files changed, 2160 insertions(+), 12 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/CollectionResourceRequestModelTests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Account/AccountStatus.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs rename Stormpath.SDK/Stormpath.SDK/Impl/{DS => DataStore}/IInternalDataStore.cs (95%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceRequestModelCompiler.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceWhereExpressionVisitor.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExpandExpressionNode.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExpandResultOperator.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExtendedQueryParser.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/FilterClause.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/FilterExpressionNode.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/AbstractAttributeTermModel.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/CollectionResourceRequestModel.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeAttributeTermModel.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/ExpansionTerm.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/OrderByModel.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/StringAttributeMatchingType.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/StringAttributeTermModel.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/WhereAttributeTermInProgressModel.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/CollectionLinkMethodNameTranslator.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/DatetimeFieldNameTranslator.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/FieldNameTranslator.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/LinkMethodNameTranslator.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/StringHelperMethodNameTranslator.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Resource/IAuditable.cs rename Stormpath.SDK/Stormpath.SDK/Resource/{ICollectionResource.cs => ICollectionResourceQueryable.cs} (82%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Resource/ICustomData.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Resource/IExtendable.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/CollectionResourceRequestModelTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/CollectionResourceRequestModelTests.cs new file mode 100644 index 00000000..16454cab --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/CollectionResourceRequestModelTests.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stormpath.SDK.Impl.Linq.RequestModel; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class CollectionResourceRequestModelTests + { + [TestClass] + public class ExpansionTermTests + { + [TestMethod] + public void Expansion_with_null_subparameters_is_valid() + { + var expansion = new ExpansionTerm("foo"); + + expansion.IsValid().ShouldBe(true); + } + + [TestMethod] + public void Expansion_with_subparameters_is_valid() + { + var expansion = new ExpansionTerm("foo", 10, 10); + + expansion.IsValid().ShouldBe(true); + } + + [TestMethod] + public void Expansion_with_empty_fieldname_is_invalid() + { + var expansion = new ExpansionTerm(string.Empty); + + expansion.IsValid().ShouldBe(false); + } + + [TestMethod] + public void Expansion_with_offset_zero_is_valid() + { + var expansion = new ExpansionTerm("foo", offset: 0); + + expansion.IsValid().ShouldBe(true); + } + + [TestMethod] + public void Expansion_with_offset_negative_is_invalid() + { + var expansion = new ExpansionTerm("foo", offset: -1); + + expansion.IsValid().ShouldBe(false); + } + + [TestMethod] + public void Expansion_with_limit_zero_is_invalid() + { + var expansion = new ExpansionTerm("foo", limit: 0); + + expansion.IsValid().ShouldBe(false); + } + + [TestMethod] + public void Expansion_with_limit_negative_is_invalid() + { + var expansion = new ExpansionTerm("foo", limit: -1); + + expansion.IsValid().ShouldBe(false); + } + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs new file mode 100644 index 00000000..f9e15c75 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs @@ -0,0 +1,177 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using Shouldly; +using Stormpath.SDK.Account; +using System; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class LinqTests + { + private static string Url = "http://f.oo"; + private static string Resource = "bar"; + + [TestClass] + public class FilterExtensionMethod + { + [TestMethod] + public void Filter_with_simple_parameter() + { + // Arrange + var harness = TestHarness.Create(Url, Resource); + + // Act + harness.Queryable + .Filter("Joe") + .ToList(); + + // Assert + harness.DataStore.Received().GetCollection($"{Url}/{Resource}?q=Joe"); + } + + [TestMethod] + public void Filter_multiple_calls_are_LIFO() + { + var harness = TestHarness.Create(Url, Resource); + + harness.Queryable + .Filter("Joe") + .Filter("Joey") + .ToList(); + + // Expected behavior: the last call will be kept + harness.DataStore.Received().GetCollection($"{Url}/{Resource}?q=Joey"); + } + } + + [TestClass] + public class ExpandExtensionMethod + { + [TestMethod] + public void Expand_one_link() + { + var harness = TestHarness.Create(Url, Resource); + + harness.Queryable + .Expand(x => x.GetDirectoryAsync()) + .ToList(); + + harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=directory"); + } + + [TestMethod] + public void Expand_multiple_links() + { + var harness = TestHarness.Create(Url, Resource); + + harness.Queryable + .Expand(x => x.GetDirectoryAsync()) + .Expand(x => x.GetTenantAsync()) + .ToList(); + + harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=directory,tenant"); + } + + [TestMethod] + public void Expand_collection_query_with_offset() + { + var harness = TestHarness.Create(Url, Resource); + + harness.Queryable + .Expand(x => x.GetGroupsAsync(), offset: 10) + .ToList(); + + harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=groups(offset:10)"); + } + + [TestMethod] + public void Expand_collection_query_with_limit() + { + var harness = TestHarness.Create(Url, Resource); + + harness.Queryable + .Expand(x => x.GetGroupsAsync(), limit: 20) + .ToList(); + + harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=groups(limit:20)"); + } + + [TestMethod] + public void Expand_collection_query_with_both_parameters() + { + var harness = TestHarness.Create(Url, Resource); + + harness.Queryable + .Expand(x => x.GetGroupsAsync(), 5, 15) + .ToList(); + + harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=groups(offset:5,limit:15)"); + } + + [TestMethod] + public void Expand_all_the_things() + { + var harness = TestHarness.Create(Url, Resource); + + harness.Queryable + .Expand(x => x.GetTenantAsync()) + .Expand(x => x.GetGroupsAsync(), 10, 20) + .Expand(x => x.GetDirectoryAsync()) + .ToList(); + + harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=tenant,groups(offset:10,limit:20),directory"); + } + + [TestMethod] + public void Expand_throws_if_used_on_an_attribute() + { + var harness = TestHarness.Create(Url, Resource); + + Should.Throw(() => { + harness.Queryable.Expand(x => x.Email).ToList(); + }); + } + + [TestMethod] + public void Expand_throws_if_parameters_are_supplied_for_link() + { + var harness = TestHarness.Create(Url, Resource); + + Should.Throw(() => + { + harness.Queryable.Expand(x => x.GetDirectoryAsync(), limit: 10).ToList(); + }); + } + + [TestMethod] + public void Expand_throws_if_syntax_is_dumb() + { + var harness = TestHarness.Create(Url, Resource); + + Should.Throw(() => + { + harness.Queryable.Expand(x => x.GetTenantAsync().GetAwaiter()).ToList(); + }); + } + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs new file mode 100644 index 00000000..fc2f2a15 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using NSubstitute; +using Stormpath.SDK.DataStore; +using Stormpath.SDK.Impl.Resource; +using Stormpath.SDK.Resource; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + public class TestHarness + where T : IResource + { + public IDataStore DataStore { get; private set; } + + public string Url { get; private set; } + + public string Resource { get; private set; } + + public ICollectionResourceQueryable Queryable { get; private set; } + + public static TestHarness Create(string url, string resource) + where TType : IResource + { + var ds = Substitute.For(); + + return new TestHarness() + { + DataStore = ds, + Resource = resource, + Url = url, + Queryable = new CollectionResourceQueryable(url, resource, ds) + }; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index b9f5923e..e19aefa1 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -42,6 +42,10 @@ ..\packages\NSubstitute.1.8.2.0\lib\net45\NSubstitute.dll True + + ..\packages\Remotion.Linq.1.15.15.0\lib\portable-net45+wp80+wpa81+win\Remotion.Linq.dll + True + ..\packages\Shouldly.2.5.0\lib\net40\Shouldly.dll True @@ -65,6 +69,9 @@ + + + diff --git a/Stormpath.SDK/Stormpath.SDK/Account/AccountStatus.cs b/Stormpath.SDK/Stormpath.SDK/Account/AccountStatus.cs new file mode 100644 index 00000000..ea34f910 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Account/AccountStatus.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Stormpath.SDK.Account +{ + public enum AccountStatus + { + /// + /// The account is able to log into assigned applications. + /// + Enabled, + + /// + /// The account cannot log into any applications. + /// + Disabled, + + /// + /// The account is disabled and has not verified their email address. + /// + Unverified + } +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Account/IAccount.cs b/Stormpath.SDK/Stormpath.SDK/Account/IAccount.cs index 27ab8c0b..4e53a0c6 100644 --- a/Stormpath.SDK/Stormpath.SDK/Account/IAccount.cs +++ b/Stormpath.SDK/Stormpath.SDK/Account/IAccount.cs @@ -15,11 +15,34 @@ // limitations under the License. // +using System.Threading.Tasks; +using Stormpath.SDK.Directory; +using Stormpath.SDK.Group; using Stormpath.SDK.Resource; +using Stormpath.SDK.Tenant; namespace Stormpath.SDK.Account { - public interface IAccount : IResource + public interface IAccount : IResource, IAuditable, IExtendable { + string Username { get; } + + string Email { get; } + + string FullName { get; } + + string GivenName { get; } + + string MiddleName { get; } + + string Surname { get; } + + AccountStatus Status { get; } + + Task GetGroupsAsync(); + + Task GetDirectoryAsync(); + + Task GetTenantAsync(); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Account/IAccountList.cs b/Stormpath.SDK/Stormpath.SDK/Account/IAccountList.cs index 72d75cbe..747c8388 100644 --- a/Stormpath.SDK/Stormpath.SDK/Account/IAccountList.cs +++ b/Stormpath.SDK/Stormpath.SDK/Account/IAccountList.cs @@ -19,7 +19,7 @@ namespace Stormpath.SDK.Account { - public interface IAccountList : ICollectionResource + public interface IAccountList : ICollectionResourceQueryable { } } diff --git a/Stormpath.SDK/Stormpath.SDK/Application/IApplicationList.cs b/Stormpath.SDK/Stormpath.SDK/Application/IApplicationList.cs index 38eba7b5..cebb3f36 100644 --- a/Stormpath.SDK/Stormpath.SDK/Application/IApplicationList.cs +++ b/Stormpath.SDK/Stormpath.SDK/Application/IApplicationList.cs @@ -19,7 +19,7 @@ namespace Stormpath.SDK.Application { - public interface IApplicationList : ICollectionResource + public interface IApplicationList : ICollectionResourceQueryable { } } diff --git a/Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs b/Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs new file mode 100644 index 00000000..ac855606 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Threading.Tasks; +using Stormpath.SDK.Resource; + +namespace Stormpath.SDK.DataStore +{ + public interface IDataStore + { + IEnumerable GetCollection(string href); + + Task> GetCollectionAsync(string href); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Directory/IDirectoryList.cs b/Stormpath.SDK/Stormpath.SDK/Directory/IDirectoryList.cs index 4db8cfef..8b5e5ba5 100644 --- a/Stormpath.SDK/Stormpath.SDK/Directory/IDirectoryList.cs +++ b/Stormpath.SDK/Stormpath.SDK/Directory/IDirectoryList.cs @@ -19,7 +19,7 @@ namespace Stormpath.SDK.Directory { - public interface IDirectoryList : ICollectionResource + public interface IDirectoryList : ICollectionResourceQueryable { } } diff --git a/Stormpath.SDK/Stormpath.SDK/Group/IGroupList.cs b/Stormpath.SDK/Stormpath.SDK/Group/IGroupList.cs index 4c60b981..94f630b9 100644 --- a/Stormpath.SDK/Stormpath.SDK/Group/IGroupList.cs +++ b/Stormpath.SDK/Stormpath.SDK/Group/IGroupList.cs @@ -19,7 +19,7 @@ namespace Stormpath.SDK.Group { - public interface IGroupList : ICollectionResource + public interface IGroupList : ICollectionResourceQueryable { } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs index ba107005..b85ebabc 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs @@ -18,7 +18,6 @@ using System; using Stormpath.SDK.Api; using Stormpath.SDK.Client; -using Stormpath.SDK.Tenant; namespace Stormpath.SDK.Impl.Client { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DS/IInternalDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IInternalDataStore.cs similarity index 95% rename from Stormpath.SDK/Stormpath.SDK/Impl/DS/IInternalDataStore.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IInternalDataStore.cs index d7bd2acb..49fd1d0c 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DS/IInternalDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IInternalDataStore.cs @@ -15,7 +15,7 @@ // limitations under the License. // -namespace Stormpath.SDK.Impl.DS +namespace Stormpath.SDK.Impl.DataStore { internal interface IInternalDataStore { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs new file mode 100644 index 00000000..605236ea --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using Remotion.Linq; +using Stormpath.SDK.DataStore; + +namespace Stormpath.SDK.Impl.Linq +{ + internal class CollectionResourceQueryExecutor : IQueryExecutor + { + private readonly string url; + private readonly string resource; + private readonly IDataStore dataStore; + + public CollectionResourceQueryExecutor(string url, string resource, IDataStore dataStore) + { + this.url = url; + this.resource = resource; + this.dataStore = dataStore; + } + + public IEnumerable ExecuteCollection(QueryModel queryModel) + { + var model = CollectionResourceQueryModelVisitor.GenerateRequestModel(queryModel); + var arguments = CollectionResourceRequestModelCompiler.GetArguments(model); + var argumentString = string.Join("&", arguments); + + return dataStore.GetCollection($"{url}/{resource}?{argumentString}"); + } + + public T ExecuteScalar(QueryModel queryModel) + { + return ExecuteCollection(queryModel).Single(); + } + + public T ExecuteSingle(QueryModel queryModel, bool returnDefaultWhenEmpty) + { + return returnDefaultWhenEmpty + ? ExecuteCollection(queryModel).SingleOrDefault() + : ExecuteCollection(queryModel).Single(); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs new file mode 100644 index 00000000..127d5029 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs @@ -0,0 +1,191 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq.Expressions; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Clauses.ResultOperators; +using Stormpath.SDK.Impl.Linq.Parsing; +using Stormpath.SDK.Impl.Linq.RequestModel; +using Stormpath.SDK.Impl.Linq.StaticNameTranslators; + +namespace Stormpath.SDK.Impl.Linq +{ + internal class CollectionResourceQueryModelVisitor : QueryModelVisitorBase + { + public CollectionResourceRequestModel ParsedModel { get; private set; } = new CollectionResourceRequestModel(); + + public static CollectionResourceRequestModel GenerateRequestModel(QueryModel queryModel) + { + var visitor = new CollectionResourceQueryModelVisitor(); + visitor.VisitQueryModel(queryModel); + return visitor.ParsedModel; + } + + public override void VisitResultOperator(ResultOperatorBase resultOperator, QueryModel queryModel, int index) + { + // Todo Any + // TODO First + // TODO Count/LongCount + // Todo DefaultIfEmpty + // Todo ElementAt[OrDefault] + // TODO Single + var takeResultOperator = resultOperator as TakeResultOperator; + if (takeResultOperator != null) + { + var expression = takeResultOperator.Count; + if (expression.NodeType == ExpressionType.Constant) + { + ParsedModel.Limit = (int)((ConstantExpression)expression).Value; + } + else + { + throw new NotSupportedException("Unsupported expression in Take clause."); + } + + return; // done + } + + var skipResultOperator = resultOperator as SkipResultOperator; + if (skipResultOperator != null) + { + var expression = skipResultOperator.Count as ConstantExpression; + if (expression == null) + throw new NotSupportedException("Unsupported expression in Skip clause."); + + ParsedModel.Offset = (int)expression.Value; + return; // done + } + + var expandResultOperator = resultOperator as ExpandResultOperator; + if (expandResultOperator != null) + { + var methodCallExpression = expandResultOperator.KeySelector as MethodCallExpression; + if (methodCallExpression == null) + throw new NotSupportedException("Expand must be used on a link property method."); + + var expandField = string.Empty; + if (LinkMethodNameTranslator.TryGetValue(methodCallExpression.Method.Name, out expandField)) + { + bool paginationParametersPresent = + expandResultOperator?.Offset?.Value != null || + expandResultOperator?.Limit?.Value != null; + if (paginationParametersPresent) + throw new NotSupportedException("Pagination options cannot be used on link-only properties."); + + ParsedModel.Expansions.Add(new ExpansionTerm(expandField)); + return; // done + } + + if (CollectionLinkMethodTranslator.TryGetValue(methodCallExpression.Method.Name, out expandField)) + { + ParsedModel.Expansions.Add(new ExpansionTerm(expandField, + (int?)expandResultOperator.Offset.Value, + (int?)expandResultOperator.Limit.Value)); + return; // done + } + + throw new NotSupportedException("The selected method does not support expansions."); + } + + bool isUnsupported = + resultOperator is AllResultOperator || + resultOperator is AggregateResultOperator || + resultOperator is AggregateFromSeedResultOperator || + resultOperator is AverageResultOperator || + resultOperator is CastResultOperator || + resultOperator is ContainsResultOperator || + resultOperator is DistinctResultOperator || + resultOperator is ExceptResultOperator || + resultOperator is GroupResultOperator || + resultOperator is IntersectResultOperator || + resultOperator is LastResultOperator || + resultOperator is MaxResultOperator || + resultOperator is MinResultOperator || + resultOperator is OfTypeResultOperator || + resultOperator is ReverseResultOperator || + resultOperator is SumResultOperator || + resultOperator is UnionResultOperator; + if (isUnsupported) + { + throw new NotSupportedException("One or more LINQ operators are not supported."); + } + + base.VisitResultOperator(resultOperator, queryModel, index); + } + + public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index) + { + // Handle custom .Filter extension method + var filterClause = whereClause as FilterClause; + if (filterClause != null) + { + ParsedModel.FilterTerm = filterClause.Term; + return; // done + } + + this.ParsedModel.AddAttributeTerms( + CollectionResourceWhereExpressionVisitor.GenerateModels(whereClause.Predicate)); + + base.VisitWhereClause(whereClause, queryModel, index); + } + + public override void VisitOrdering(Ordering ordering, QueryModel queryModel, OrderByClause orderByClause, int index) + { + var memberAccessor = ordering.Expression as MemberExpression; + if (memberAccessor == null) + throw new NotSupportedException("Only resource fields are supported in OrderBy."); + + var fieldName = string.Empty; + if (!FieldNameTranslator.TryGetValue(memberAccessor.Member.Name, out fieldName)) + throw new NotSupportedException($"The field {memberAccessor.Member.Name} is not supported in OrderBy."); + + this.ParsedModel.AddOrderBy(fieldName, descending: ordering.OrderingDirection == OrderingDirection.Desc); + } + + public override void VisitSelectClause(SelectClause selectClause, QueryModel queryModel) + { + // We have to determine if this Select is being called internally by a LINQ filter like Take + // which essentially appends .Select(x => x) at the end of the query. If so, it's fine. + // TODO This also applies to LINQ query sytax: from x select x is fine + bool isInternalSelect = queryModel.MainFromClause == + (selectClause.Selector as QuerySourceReferenceExpression)?.ReferencedQuerySource as MainFromClause; + + if (!isInternalSelect) + throw new NotSupportedException("Select is not supported."); + + base.VisitSelectClause(selectClause, queryModel); + } + + public override void VisitGroupJoinClause(GroupJoinClause groupJoinClause, QueryModel queryModel, int index) + { + throw new NotSupportedException("GroupJoin is not supported."); + } + + public override void VisitJoinClause(JoinClause joinClause, QueryModel queryModel, int index) + { + throw new NotSupportedException("Join is not supported."); + } + + public override void VisitJoinClause(JoinClause joinClause, QueryModel queryModel, GroupJoinClause groupJoinClause) + { + throw new NotSupportedException("Join is not supported."); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceRequestModelCompiler.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceRequestModelCompiler.cs new file mode 100644 index 00000000..e84de0b8 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceRequestModelCompiler.cs @@ -0,0 +1,222 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Stormpath.SDK.Impl.Linq.RequestModel; +using Stormpath.SDK.Impl.Utility; + +namespace Stormpath.SDK.Impl.Linq +{ + internal static class CollectionResourceRequestModelCompiler + { + public static List GetArguments(CollectionResourceRequestModel model) + { + var arguments = new Dictionary(); + + // From .Filter() + if (!string.IsNullOrEmpty(model.FilterTerm)) + arguments.Add("q", model.FilterTerm); + + // From .Where(x => ?) + if (model.AttributeTerms.Count > 0) + { + foreach (var term in model.AttributeTerms) + { + switch (term.MatchType) + { + case StringAttributeMatchingType.Contains: + arguments.Add(term.Field, $"*{term.Value}*"); + break; + case StringAttributeMatchingType.EndsWith: + arguments.Add(term.Field, $"*{term.Value}"); + break; + case StringAttributeMatchingType.StartsWith: + arguments.Add(term.Field, $"{term.Value}*"); + break; + case StringAttributeMatchingType.Equals: + default: + arguments.Add(term.Field, term.Value); + break; + } + } + } + + // From .Where(x => [createdAt|modifiedAt] >= ??) + if (model.DatetimeAttributeTerms.Count > 0) + { + var aggregatedTerms = AggregateDatetimeTerms(model.DatetimeAttributeTerms); + + foreach (var term in aggregatedTerms) + { + var datetimeAttribute = new StringBuilder(); + + if (!term.Start.HasValue) + { + datetimeAttribute.Append("["); + } + else + { + datetimeAttribute.Append(term.StartInclusive ?? true ? "[" : "("); + datetimeAttribute.Append(Iso8601.Format(term.Start.Value)); + } + + datetimeAttribute.Append(","); + + if (!term.End.HasValue) + { + datetimeAttribute.Append("]"); + } + else + { + datetimeAttribute.Append(Iso8601.Format(term.End.Value)); + datetimeAttribute.Append(term.EndInclusive ?? true ? "]" : ")"); + } + + arguments.Add(term.Field, datetimeAttribute.ToString()); + } + } + + // From .Take() + if (model?.Limit > 0) + arguments.Add("limit", model.Limit.Value.ToString()); + + // From .Skip() + if (model?.Offset > 0) + arguments.Add("offset", model.Offset.Value.ToString()); + + // From .OrderBy(x => ?) / .OrderByDescending(x => ?) + if (model.OrderByTerms.Count > 0) + { + var orderByArgument = new StringBuilder(); + bool addedOne = false; + + foreach (var clause in model.OrderByTerms) + { + if (addedOne) + orderByArgument.Append(","); + var direction = clause.Descending ? " desc" : string.Empty; + orderByArgument.Append($"{clause.Field}{direction}"); + addedOne = true; + } + + if (addedOne) + arguments.Add("orderBy", orderByArgument.ToString()); + } + + // From .Expand() + if (model.Expansions.Count > 0) + { + var expansionArgument = new StringBuilder(); + bool addedOne = false; + + foreach (var item in model.Expansions) + { + if (!item.IsValid()) // && item.CanBeUsedForThisResource + throw new ArgumentException($"Expansion term '{item.Field}' is not valid."); + + if (addedOne) + expansionArgument.Append(","); + + bool hasSubparameters = item.Offset.HasValue || item.Limit.HasValue; + expansionArgument.Append(item.Field); + + if (hasSubparameters) + expansionArgument.Append("("); + + if (item.Offset.HasValue) + expansionArgument.Append($"offset:{item.Offset.Value}"); + + if (item.Limit.HasValue) + { + if (item.Offset.HasValue) + expansionArgument.Append(","); + expansionArgument.Append($"limit:{item.Limit.Value}"); + } + + if (hasSubparameters) + expansionArgument.Append(")"); + + addedOne = true; + } + + if (addedOne) + arguments.Add("expand", expansionArgument.ToString()); + } + + var argumentList = arguments + .Select(x => $"{x.Key}={x.Value}") + .ToList(); + + return argumentList; + } + + private static List AggregateDatetimeTerms(IEnumerable terms) + { + // If a query has multiple terms, they'll be generated like so: + // ## a search with both starting and ending dates specified + // [0] field: 'createdAt', start: DateTimeOffset, startInclusive: true, end: null, endInclusive: null + // [1] field: 'createdAt', start: null, startInclusive: null, end: DateTimeOffset, endInclusive: true + // ## a search with only one date specified + // [3] field: 'modifiedAt', start: DateTimeOffset, startInclusive: false, end: null, endInclusive: null + // + // We need to aggregate this down to 1 term for each field. + // TODO: refactor this to be less dumb and make this method unnecessary. + var workingModels = new Dictionary(); + + foreach (var term in terms) + { + if (!workingModels.ContainsKey(term.Field)) + workingModels.Add(term.Field, new DatetimeAttributeTermWorkingModel()); + var workingModel = workingModels[term.Field]; + + bool collision = + (term.Start.HasValue && workingModel.Start.HasValue) || + (term.StartInclusive.HasValue && workingModel.StartInclusive.HasValue) || + (term.End.HasValue && workingModel.End.HasValue) || + (term.EndInclusive.HasValue && workingModel.EndInclusive.HasValue); + if (collision) + throw new ArgumentException("Error compiling date terms."); + + workingModel.Field = term.Field; + workingModel.Start = term.Start ?? workingModel.Start; + workingModel.StartInclusive = term.StartInclusive ?? workingModel.StartInclusive; + workingModel.End = term.End ?? workingModel.End; + workingModel.EndInclusive = term.EndInclusive ?? workingModel.EndInclusive; + } + + return workingModels + .Select(kvp => new DatetimeAttributeTermModel(kvp.Value.Field, kvp.Value.Start, kvp.Value.StartInclusive, kvp.Value.End, kvp.Value.EndInclusive)) + .ToList(); + } + + private class DatetimeAttributeTermWorkingModel + { + public string Field { get; set; } + + public DateTimeOffset? Start { get; set; } + + public bool? StartInclusive { get; set; } + + public DateTimeOffset? End { get; set; } + + public bool? EndInclusive { get; set; } + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceWhereExpressionVisitor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceWhereExpressionVisitor.cs new file mode 100644 index 00000000..a7579e77 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceWhereExpressionVisitor.cs @@ -0,0 +1,194 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Remotion.Linq.Clauses.ExpressionTreeVisitors; +using Remotion.Linq.Parsing; +using Stormpath.SDK.Impl.Linq.RequestModel; +using Stormpath.SDK.Impl.Linq.StaticNameTranslators; + +namespace Stormpath.SDK.Impl.Linq +{ + internal class CollectionResourceWhereExpressionVisitor : ThrowingExpressionTreeVisitor + { + private WhereAttributeTermInProgressModel inProgress = new WhereAttributeTermInProgressModel(); + + public List GeneratedModels { get; } = new List(); + + public static List GenerateModels(Expression expression) + { + var visitor = new CollectionResourceWhereExpressionVisitor(); + visitor.VisitExpression(expression); + return visitor.GeneratedModels; + } + + public override Expression VisitExpression(Expression expression) + { + var visited = base.VisitExpression(expression); + + if (inProgress.IsStringTermComplete()) + { + this.GeneratedModels.Add(new StringAttributeTermModel(inProgress.Field, inProgress.StringValue, inProgress.StringMatchType.Value)); + } + + if (inProgress.IsDateTermComplete()) + { + DatetimeAttributeTermModel model = null; + if (inProgress.DateGreaterThan.Value) + { + model = new DatetimeAttributeTermModel( + inProgress.Field, + start: inProgress.DateValue.Value, + startInclusive: inProgress.DateOrEqual.Value); + } + else + { + model = new DatetimeAttributeTermModel( + inProgress.Field, + end: inProgress.DateValue.Value, + endInclusive: inProgress.DateOrEqual.Value); + } + + this.GeneratedModels.Add(model); + } + + return visited; + } + + protected override Expression VisitBinaryExpression(BinaryExpression expression) + { + if (!IsSupportedBinaryComparisonOperator(expression.NodeType)) + throw new NotSupportedException("One or more comparison operators are currently unsupported."); + + if (expression.NodeType == ExpressionType.AndAlso) + { + this.GeneratedModels.AddRange(GenerateModels(expression.Left)); + this.GeneratedModels.AddRange(GenerateModels(expression.Right)); + return expression; // done + } + + bool isInequalityComparison = + expression.NodeType == ExpressionType.GreaterThan || + expression.NodeType == ExpressionType.GreaterThanOrEqual || + expression.NodeType == ExpressionType.LessThan || + expression.NodeType == ExpressionType.LessThanOrEqual; + if (isInequalityComparison) + { + // Only supported on dates right now + inProgress.DateGreaterThan = expression.NodeType == ExpressionType.GreaterThan || expression.NodeType == ExpressionType.GreaterThanOrEqual; + inProgress.DateOrEqual = expression.NodeType == ExpressionType.GreaterThanOrEqual || expression.NodeType == ExpressionType.LessThanOrEqual; + } + + VisitExpression(expression.Left); + VisitExpression(expression.Right); + + return expression; + } + + private bool IsSupportedBinaryComparisonOperator(ExpressionType type) + { + return + type == ExpressionType.And || + type == ExpressionType.AndAlso || + type == ExpressionType.GreaterThan || + type == ExpressionType.GreaterThanOrEqual || + type == ExpressionType.LessThan || + type == ExpressionType.LessThanOrEqual || + type == ExpressionType.Equal; + } + + protected override Expression VisitMethodCallExpression(MethodCallExpression expression) + { + StringAttributeMatchingType matchingType; + bool isAChainedHelperMethodCall = StringHelperMethodNameTranslator.TryGetValue(expression.Method.Name, out matchingType); + if (isAChainedHelperMethodCall) + { + if (expression.Object.NodeType != ExpressionType.MemberAccess) + throw new NotSupportedException("Chained method calls can only be used on attribute properties in the Where clause."); + + if (expression.Arguments[0].NodeType != ExpressionType.Constant) + throw new NotSupportedException("Only constant values may be used in a method call inside the Where clause."); + + if (expression.Arguments.Count > 1) + throw new NotSupportedException("Only simple overloads of helper methods may be used inside the Where clause."); + + inProgress.StringMatchType = matchingType; + + VisitMemberExpression((MemberExpression)expression.Object); + VisitConstantExpression((ConstantExpression)expression.Arguments[0]); + return expression; + } + + return base.VisitMethodCallExpression(expression); // throws + } + + protected override Expression VisitMemberExpression(MemberExpression expression) + { + var fieldName = string.Empty; + + if (DatetimeFieldNameTranslator.TryGetValue(expression.Member.Name, out fieldName)) + { + inProgress.TargetType = typeof(DatetimeAttributeTermModel); + inProgress.Field = fieldName; + return expression; // done + } + + if (FieldNameTranslator.TryGetValue(expression.Member.Name, out fieldName)) + { + inProgress.TargetType = typeof(StringAttributeTermModel); + inProgress.Field = fieldName; + return expression; // done + } + + throw new NotSupportedException($"The property {expression.Member.Name} is not currently supported in a Where clause."); + } + + protected override Expression VisitConstantExpression(ConstantExpression expression) + { + if (expression.Type == typeof(string)) + { + inProgress.StringValue = expression.Value.ToString(); + return expression; // done + } + + if (expression.Type == typeof(DateTimeOffset)) + { + inProgress.DateValue = (DateTimeOffset)expression.Value; + return expression; // done + } + + throw new NotSupportedException($"The constant type {expression.Type.Name} is not currently supported in a Where clause."); + } + + // TODO + // Called when a LINQ expression type is not handled above. + protected override Exception CreateUnhandledItemException(T unhandledItem, string visitMethod) + { + string itemText = FormatUnhandledItem(unhandledItem); + var message = string.Format("The expression '{0}' (type: {1}) is not supported by this LINQ provider.", itemText, typeof(T)); + return new NotSupportedException(message); + } + + private static string FormatUnhandledItem(T unhandledItem) + { + var itemAsExpression = unhandledItem as Expression; + return itemAsExpression != null ? FormattingExpressionTreeVisitor.Format(itemAsExpression) : unhandledItem.ToString(); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExpandExpressionNode.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExpandExpressionNode.cs new file mode 100644 index 00000000..89bb593e --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExpandExpressionNode.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Remotion.Linq.Clauses; +using Remotion.Linq.Parsing.Structure.IntermediateModel; + +namespace Stormpath.SDK.Impl.Linq.Parsing +{ + internal class ExpandExpressionNode : ResultOperatorExpressionNodeBase + { + private readonly LambdaExpression keySelectorLambda; + private readonly ConstantExpression offset = null; + private readonly ConstantExpression limit = null; + + public ExpandExpressionNode(MethodCallExpressionParseInfo parseInfo, LambdaExpression keySelector, ConstantExpression offset = null, ConstantExpression limit = null) + : base(parseInfo, null, null) + { + this.keySelectorLambda = keySelector; + this.offset = offset; + this.limit = limit; + } + + public static MethodInfo[] SupportedMethods + { + get + { + return typeof(CollectionResourceQueryableExpandExtensions).GetMethods().ToArray(); + } + } + + public override Expression Resolve(ParameterExpression inputParameter, Expression expressionToBeResolved, ClauseGenerationContext clauseGenerationContext) + { + return Source.Resolve(inputParameter, expressionToBeResolved, clauseGenerationContext); + } + + protected override ResultOperatorBase CreateResultOperator(ClauseGenerationContext clauseGenerationContext) + { + var resolvedParameter = Source + .Resolve(this.keySelectorLambda.Parameters[0], this.keySelectorLambda.Body, clauseGenerationContext); + return new ExpandResultOperator(resolvedParameter, this.offset, this.limit); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExpandResultOperator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExpandResultOperator.cs new file mode 100644 index 00000000..b3164e2e --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExpandResultOperator.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq.Expressions; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.ExpressionTreeVisitors; +using Remotion.Linq.Clauses.ResultOperators; +using Remotion.Linq.Clauses.StreamedData; + +namespace Stormpath.SDK.Impl.Linq.Parsing +{ + internal class ExpandResultOperator : SequenceTypePreservingResultOperatorBase + { + public ExpandResultOperator(Expression keySelector, ConstantExpression offset, ConstantExpression limit) + { + this.KeySelector = keySelector; + this.Offset = offset; + this.Limit = limit; + } + + public Expression KeySelector { get; private set; } + + public ConstantExpression Offset { get; private set; } + + public ConstantExpression Limit { get; private set; } + + public override string ToString() + { + return $"Expand({FormattingExpressionTreeVisitor.Format(this.KeySelector)}, offset: {this.Offset}, limit: {this.Limit})"; + } + + public override StreamedSequence ExecuteInMemory(StreamedSequence input) + { + return input; // (does not mutate the sequence) + } + + public override ResultOperatorBase Clone(CloneContext cloneContext) + { + return new ExpandResultOperator(this.KeySelector, this.Offset, this.Limit); + } + + public override void TransformExpressions(Func transformation) + { + this.KeySelector = transformation(this.KeySelector); + } + } +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExtendedQueryParser.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExtendedQueryParser.cs new file mode 100644 index 00000000..90b1ba66 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/ExtendedQueryParser.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Remotion.Linq.Parsing.ExpressionTreeVisitors.Transformation; +using Remotion.Linq.Parsing.Structure; +using Remotion.Linq.Parsing.Structure.NodeTypeProviders; + +namespace Stormpath.SDK.Impl.Linq.Parsing +{ + internal static class ExtendedQueryParser + { + public static IQueryParser Create() + { + var processor = ExpressionTreeParser.CreateDefaultProcessor( + ExpressionTransformerRegistry.CreateDefault()); + var nodeTypeProvider = ExpressionTreeParser.CreateDefaultNodeTypeProvider(); + + // Add the nodes that handle our custom extension methods + var customRegistry = new MethodInfoBasedNodeTypeRegistry(); + customRegistry.Register(ExpandExpressionNode.SupportedMethods, typeof(ExpandExpressionNode)); + customRegistry.Register(FilterExpressionNode.SupportedMethods, typeof(FilterExpressionNode)); + nodeTypeProvider.InnerProviders.Add(customRegistry); + + var expressionTreeParser = new ExpressionTreeParser(nodeTypeProvider, processor); + return new QueryParser(expressionTreeParser); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/FilterClause.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/FilterClause.cs new file mode 100644 index 00000000..473b5174 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/FilterClause.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq.Expressions; +using Remotion.Linq.Clauses; + +namespace Stormpath.SDK.Impl.Linq.Parsing +{ + internal class FilterClause : WhereClause + { + public FilterClause(Expression predicate) + : base(predicate) + { + var filterTerm = predicate as ConstantExpression; + if (filterTerm == null) + throw new NotSupportedException("Filter must operate on a constant value."); + + var stringTerm = (string)filterTerm.Value; + if (string.IsNullOrEmpty(stringTerm)) + throw new NotSupportedException("Filter term cannot be empty."); + this.Term = stringTerm; + } + + public string Term { get; private set; } + + public override string ToString() + { + return $"Filter({this.Term}"; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/FilterExpressionNode.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/FilterExpressionNode.cs new file mode 100644 index 00000000..fe8a8653 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/Parsing/FilterExpressionNode.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Remotion.Linq; +using Remotion.Linq.Parsing.Structure.IntermediateModel; + +namespace Stormpath.SDK.Impl.Linq.Parsing +{ + public class FilterExpressionNode : MethodCallExpressionNodeBase + { + private readonly ConstantExpression filterTerm; + + public FilterExpressionNode(MethodCallExpressionParseInfo parseInfo, ConstantExpression filterTerm) + : base(parseInfo) + { + this.filterTerm = filterTerm; + } + + public static MethodInfo[] SupportedMethods + { + get + { + return typeof(CollectionResourceQueryableFilterExtensions).GetMethods().ToArray(); + } + } + + public override Expression Resolve(ParameterExpression inputParameter, Expression expressionToBeResolved, ClauseGenerationContext clauseGenerationContext) + { + return Source.Resolve(inputParameter, expressionToBeResolved, clauseGenerationContext); + } + + protected override QueryModel ApplyNodeSpecificSemantics(QueryModel queryModel, ClauseGenerationContext clauseGenerationContext) + { + queryModel.BodyClauses.Add(new FilterClause(filterTerm)); + return queryModel; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/AbstractAttributeTermModel.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/AbstractAttributeTermModel.cs new file mode 100644 index 00000000..5ff34da4 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/AbstractAttributeTermModel.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Stormpath.SDK.Impl.Linq.RequestModel +{ + internal abstract class AbstractAttributeTermModel + { + public string Field { get; protected set; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/CollectionResourceRequestModel.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/CollectionResourceRequestModel.cs new file mode 100644 index 00000000..4d929bb6 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/CollectionResourceRequestModel.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; + +namespace Stormpath.SDK.Impl.Linq.RequestModel +{ + internal class CollectionResourceRequestModel + { + public int? Offset { get; set; } + + public int? Limit { get; set; } + + public string FilterTerm { get; set; } + + public List AttributeTerms { get; } = new List(); + + public List DatetimeAttributeTerms { get; } = new List(); + + public List OrderByTerms { get; } = new List(); + + public List Expansions { get; } = new List(); + + public void AddAttributeTerm(AbstractAttributeTermModel model) + { + var modelAsString = model as StringAttributeTermModel; + if (modelAsString != null) + { + this.AttributeTerms.Add(modelAsString); + return; // done + } + + var modelAsDatetime = model as DatetimeAttributeTermModel; + if (modelAsDatetime != null) + { + this.DatetimeAttributeTerms.Add(modelAsDatetime); + return; // done + } + + throw new NotSupportedException("Unknown attribute term model type."); + } + + public void AddAttributeTerms(IEnumerable models) + { + foreach (var model in models) + { + AddAttributeTerm(model); + } + } + + public void AddDatetimeAttributeTerm(DatetimeAttributeTermModel model) + { + this.DatetimeAttributeTerms.Add(model); + } + + public void AddOrderBy(string field, bool descending = false) + { + this.OrderByTerms.Add(new OrderByModel(field, descending)); + } + + public void AddExpansion(string field, int? offset = null, int? limit = null) + { + this.Expansions.Add(new ExpansionTerm(field, offset, limit)); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeAttributeTermModel.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeAttributeTermModel.cs new file mode 100644 index 00000000..d4d2cc1d --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeAttributeTermModel.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Stormpath.SDK.Impl.Linq.RequestModel +{ + internal class DatetimeAttributeTermModel : AbstractAttributeTermModel + { + public DatetimeAttributeTermModel( + string field, + DateTimeOffset? start = null, + bool? startInclusive = null, + DateTimeOffset? end = null, + bool? endInclusive = null) + { + this.Field = field; + + if (start.HasValue) + { + this.Start = start; + this.StartInclusive = startInclusive; + } + + if (end.HasValue) + { + this.End = end; + this.EndInclusive = endInclusive; + } + } + + public DateTimeOffset? Start { get; private set; } + + public bool? StartInclusive { get; private set; } + + public DateTimeOffset? End { get; private set; } + + public bool? EndInclusive { get; private set; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/ExpansionTerm.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/ExpansionTerm.cs new file mode 100644 index 00000000..f8f2b41e --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/ExpansionTerm.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Stormpath.SDK.Impl.Linq.RequestModel +{ + internal class ExpansionTerm + { + public ExpansionTerm(string field, int? offset = null, int? limit = null) + { + this.Field = field; + this.Offset = offset; + this.Limit = limit; + } + + public string Field { get; private set; } + + public int? Offset { get; private set; } + + public int? Limit { get; private set; } + + public bool IsValid() + { + if (string.IsNullOrEmpty(this.Field)) return false; + + if (this.Offset.HasValue && this.Offset.Value < 0) return false; + + if (this.Limit.HasValue && this.Limit.Value <= 0) return false; + + return true; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/OrderByModel.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/OrderByModel.cs new file mode 100644 index 00000000..aa700849 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/OrderByModel.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Stormpath.SDK.Impl.Linq.RequestModel +{ + internal class OrderByModel + { + public OrderByModel(string field, bool descending) + { + this.Field = field; + this.Descending = descending; + } + + public string Field { get; private set; } + + public bool Descending { get; private set; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/StringAttributeMatchingType.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/StringAttributeMatchingType.cs new file mode 100644 index 00000000..0c090d82 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/StringAttributeMatchingType.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Stormpath.SDK.Impl.Linq.RequestModel +{ + public enum StringAttributeMatchingType + { + /// + /// Finds matches via direct case-insensitive search. + /// + Equals, + + /// + /// Finds matches that start with the specified case-insensitive string. + /// + StartsWith, + + /// + /// Finds matches that end with the specified case-insensitive string. + /// + EndsWith, + + /// + /// Finds matches that contain the specified case-insensitive string. + /// + Contains + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/StringAttributeTermModel.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/StringAttributeTermModel.cs new file mode 100644 index 00000000..48b5fbaa --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/StringAttributeTermModel.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Stormpath.SDK.Impl.Linq.RequestModel +{ + internal class StringAttributeTermModel : AbstractAttributeTermModel + { + public StringAttributeTermModel(string field, string value, StringAttributeMatchingType type) + { + this.Field = field; + this.Value = value; + this.MatchType = type; + } + + public string Value { get; private set; } + + public StringAttributeMatchingType MatchType { get; private set; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/WhereAttributeTermInProgressModel.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/WhereAttributeTermInProgressModel.cs new file mode 100644 index 00000000..eee1ffcb --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/WhereAttributeTermInProgressModel.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Stormpath.SDK.Impl.Linq.RequestModel +{ + internal class WhereAttributeTermInProgressModel + { + public string Field { get; set; } + + public Type TargetType { get; set; } + + // Used when building an eventual StringAttributeTermModel + public string StringValue { get; set; } + + public StringAttributeMatchingType? StringMatchType { get; set; } = StringAttributeMatchingType.Equals; + + // Used when building an eventual DatetimeAttributeTermModel + public DateTimeOffset? DateValue { get; set; } + + public bool? DateGreaterThan { get; set; } + + public bool? DateOrEqual { get; set; } + + public bool IsStringTermComplete() + { + return TargetType == typeof(StringAttributeTermModel) && + !string.IsNullOrEmpty(StringValue) && + StringMatchType.HasValue; + } + + public bool IsDateTermComplete() + { + return TargetType == typeof(DatetimeAttributeTermModel) && + DateValue.HasValue && + DateGreaterThan.HasValue && + DateOrEqual.HasValue; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/CollectionLinkMethodNameTranslator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/CollectionLinkMethodNameTranslator.cs new file mode 100644 index 00000000..b99a2390 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/CollectionLinkMethodNameTranslator.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; + +namespace Stormpath.SDK.Impl.Linq.StaticNameTranslators +{ + internal static class CollectionLinkMethodTranslator + { + private static readonly Dictionary ValidNames = new Dictionary() + { + { "GetGroupsAsync", "groups" }, + }; + + public static bool TryGetValue(string methodName, out string fieldName) + { + return ValidNames.TryGetValue(methodName, out fieldName); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/DatetimeFieldNameTranslator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/DatetimeFieldNameTranslator.cs new file mode 100644 index 00000000..3fad7102 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/DatetimeFieldNameTranslator.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; + +namespace Stormpath.SDK.Impl.Linq.StaticNameTranslators +{ + internal static class DatetimeFieldNameTranslator + { + private static readonly Dictionary ValidNames = new Dictionary() + { + { "CreatedAt", "createdAt" }, + { "ModifiedAt", "modifiedAt" } + }; + + public static bool TryGetValue(string methodName, out string fieldName) + { + return ValidNames.TryGetValue(methodName, out fieldName); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/FieldNameTranslator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/FieldNameTranslator.cs new file mode 100644 index 00000000..24fe8808 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/FieldNameTranslator.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; + +namespace Stormpath.SDK.Impl.Linq.StaticNameTranslators +{ + public static class FieldNameTranslator + { + private static readonly Dictionary ValidNames = new Dictionary() + { + { "Email", "email" }, + { "GivenName", "givenName" }, + { "MiddleName", "middleName" }, + { "Surname", "surname" }, + { "Username", "username" } + }; + + public static bool TryGetValue(string methodName, out string fieldName) + { + return ValidNames.TryGetValue(methodName, out fieldName); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/LinkMethodNameTranslator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/LinkMethodNameTranslator.cs new file mode 100644 index 00000000..9a891490 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/LinkMethodNameTranslator.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; + +namespace Stormpath.SDK.Impl.Linq.StaticNameTranslators +{ + internal static class LinkMethodNameTranslator + { + private static readonly Dictionary ValidNames = new Dictionary() + { + { "GetDirectoryAsync", "directory" }, + { "GetTenantAsync", "tenant" } + }; + + public static bool TryGetValue(string methodName, out string fieldName) + { + return ValidNames.TryGetValue(methodName, out fieldName); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/StringHelperMethodNameTranslator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/StringHelperMethodNameTranslator.cs new file mode 100644 index 00000000..f3511f95 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/StringHelperMethodNameTranslator.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using Stormpath.SDK.Impl.Linq.RequestModel; + +namespace Stormpath.SDK.Impl.Linq.StaticNameTranslators +{ + internal static class StringHelperMethodNameTranslator + { + private static readonly Dictionary + ValidNames = new Dictionary() + { + { "Equals", StringAttributeMatchingType.Equals }, + { "StartsWith", StringAttributeMatchingType.StartsWith }, + { "EndsWith", StringAttributeMatchingType.EndsWith }, + { "Contains", StringAttributeMatchingType.Contains } + }; + + public static bool TryGetValue(string methodName, out StringAttributeMatchingType matchingType) + { + return ValidNames.TryGetValue(methodName, out matchingType); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs new file mode 100644 index 00000000..e765b334 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using System.Linq.Expressions; +using Remotion.Linq; +using Stormpath.SDK.DataStore; +using Stormpath.SDK.Impl.Linq; +using Stormpath.SDK.Impl.Linq.Parsing; +using Stormpath.SDK.Resource; + +namespace Stormpath.SDK.Impl.Resource +{ + internal class CollectionResourceQueryable : QueryableBase, ICollectionResourceQueryable + where T : IResource + { + public CollectionResourceQueryable(string url, string resource, IDataStore dataStore) + : base(ExtendedQueryParser.Create(), CreateQueryExecutor(url, resource, dataStore)) + { + this.Url = url; + this.Resource = resource; + this.DataStore = dataStore; + } + + // This constructor is called internally by LINQ + public CollectionResourceQueryable(IQueryProvider provider, Expression expression) + : base(provider, expression) + { + } + + public string Url { get; private set; } + + public string Resource { get; private set; } + + public IDataStore DataStore { get; private set; } + + int ICollectionResourceQueryable.Offset + { + get + { + throw new NotImplementedException(); + } + } + + int ICollectionResourceQueryable.Limit + { + get + { + throw new NotImplementedException(); + } + } + + int ICollectionResourceQueryable.Size + { + get + { + throw new NotImplementedException(); + } + } + + private static IQueryExecutor CreateQueryExecutor(string url, string resource, IDataStore dataStore) + { + return new CollectionResourceQueryExecutor(url, resource, dataStore); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs b/Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs new file mode 100644 index 00000000..0c691b86 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq.Expressions; +using System.Reflection; +using Stormpath.SDK.Resource; + +// Placed in the base library namespace so that these extension methods are available without any extra usings +namespace Stormpath.SDK +{ + public static class CollectionResourceQueryableFilterExtensions + { + public static ICollectionResourceQueryable Filter(this ICollectionResourceQueryable source, string caseInsensitiveMatch) + where T : IResource + { + return (ICollectionResourceQueryable)source.Provider.CreateQuery( + Expression.Call( + ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod(typeof(T)), + source.Expression, + Expression.Constant(caseInsensitiveMatch))); + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single class", Justification = "")] + public static class CollectionResourceQueryableExpandExtensions + { + public static ICollectionResourceQueryable Expand(this ICollectionResourceQueryable source, Expression> keySelector) + where T : IResource + { + return (ICollectionResourceQueryable)source.Provider.CreateQuery( + Expression.Call( + ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod(typeof(T), typeof(TKey)), + source.Expression, + Expression.Quote(keySelector))); + } + + public static ICollectionResourceQueryable Expand(this ICollectionResourceQueryable source, Expression> keySelector, int? offset = null, int? limit = null) + where T : IResource + { + return (ICollectionResourceQueryable)source.Provider.CreateQuery( + Expression.Call( + ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod(typeof(T), typeof(TKey)), + source.Expression, + Expression.Quote(keySelector), + Expression.Constant(offset, typeof(int?)), + Expression.Constant(limit, typeof(int?)))); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/IAuditable.cs b/Stormpath.SDK/Stormpath.SDK/Resource/IAuditable.cs new file mode 100644 index 00000000..386eedd9 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Resource/IAuditable.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Stormpath.SDK.Resource +{ + public interface IAuditable + { + DateTimeOffset CreatedAt { get; } + + DateTimeOffset ModifiedAt { get; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResource.cs b/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs similarity index 82% rename from Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResource.cs rename to Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs index 27134408..a9b49c78 100644 --- a/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResource.cs +++ b/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -15,11 +15,11 @@ // limitations under the License. // -using System.Collections.Generic; +using System.Linq; namespace Stormpath.SDK.Resource { - public interface ICollectionResource : IEnumerable + public interface ICollectionResourceQueryable : IQueryable where T : IResource { int Offset { get; } diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/ICustomData.cs b/Stormpath.SDK/Stormpath.SDK/Resource/ICustomData.cs new file mode 100644 index 00000000..f8a9a15f --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Resource/ICustomData.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; + +namespace Stormpath.SDK.Resource +{ + public interface ICustomData : IAuditable, IDictionary + { + } +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/IExtendable.cs b/Stormpath.SDK/Stormpath.SDK/Resource/IExtendable.cs new file mode 100644 index 00000000..09c085cf --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Resource/IExtendable.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; + +namespace Stormpath.SDK.Resource +{ + public interface IExtendable + { + Task GetCustomDataAsync(); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 44fe2e78..7fade7a4 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -36,6 +36,10 @@ Stormpath.SDK.ruleset + + ..\packages\Remotion.Linq.1.15.15.0\lib\portable-net45+wp80+wpa81+win\Remotion.Linq.dll + True + @@ -47,6 +51,7 @@ + @@ -58,6 +63,7 @@ + @@ -66,14 +72,41 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -84,7 +117,7 @@ - + diff --git a/Stormpath.SDK/Stormpath.SDK/packages.config b/Stormpath.SDK/Stormpath.SDK/packages.config index 8b52b2f5..91978a36 100644 --- a/Stormpath.SDK/Stormpath.SDK/packages.config +++ b/Stormpath.SDK/Stormpath.SDK/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file From 6023d153b6cab1630df470985e55e6c065a0c901 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Sun, 9 Aug 2015 17:41:36 -0700 Subject: [PATCH 024/238] First pass at LINQ provider, part 2 --- .../Impl/Linq/LinqTests.cs | 737 +++++++++++++++++- .../Stormpath.SDK.Tests/packages.config | 1 + ...ollectionResourceWhereExpressionVisitor.cs | 2 + .../Resource/CollectionResourceQueryable.cs | 7 +- .../Stormpath.SDK/Resource/Shorthand.cs | 175 +++++ .../Stormpath.SDK/Stormpath.SDK.csproj | 1 + Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs | 3 +- 7 files changed, 898 insertions(+), 28 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs index f9e15c75..304e2d97 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs @@ -15,20 +15,22 @@ // limitations under the License. // +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using Shouldly; using Stormpath.SDK.Account; -using System; +using Stormpath.SDK.Resource; namespace Stormpath.SDK.Tests.Impl.Linq { [TestClass] public class LinqTests { - private static string Url = "http://f.oo"; - private static string Resource = "bar"; + private static string url = "http://f.oo"; + private static string resource = "bar"; [TestClass] public class FilterExtensionMethod @@ -37,7 +39,7 @@ public class FilterExtensionMethod public void Filter_with_simple_parameter() { // Arrange - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); // Act harness.Queryable @@ -45,13 +47,13 @@ public void Filter_with_simple_parameter() .ToList(); // Assert - harness.DataStore.Received().GetCollection($"{Url}/{Resource}?q=Joe"); + harness.WasCalledWithArguments("q=Joe"); } [TestMethod] public void Filter_multiple_calls_are_LIFO() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); harness.Queryable .Filter("Joe") @@ -59,7 +61,7 @@ public void Filter_multiple_calls_are_LIFO() .ToList(); // Expected behavior: the last call will be kept - harness.DataStore.Received().GetCollection($"{Url}/{Resource}?q=Joey"); + harness.WasCalledWithArguments("q=Joey"); } } @@ -69,68 +71,68 @@ public class ExpandExtensionMethod [TestMethod] public void Expand_one_link() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetDirectoryAsync()) .ToList(); - harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=directory"); + harness.WasCalledWithArguments("expand=directory"); } [TestMethod] public void Expand_multiple_links() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetDirectoryAsync()) .Expand(x => x.GetTenantAsync()) .ToList(); - harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=directory,tenant"); + harness.WasCalledWithArguments("expand=directory,tenant"); } [TestMethod] public void Expand_collection_query_with_offset() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetGroupsAsync(), offset: 10) .ToList(); - harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=groups(offset:10)"); + harness.WasCalledWithArguments("expand=groups(offset:10)"); } [TestMethod] public void Expand_collection_query_with_limit() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetGroupsAsync(), limit: 20) .ToList(); - harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=groups(limit:20)"); + harness.WasCalledWithArguments("expand=groups(limit:20)"); } [TestMethod] public void Expand_collection_query_with_both_parameters() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetGroupsAsync(), 5, 15) .ToList(); - harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=groups(offset:5,limit:15)"); + harness.WasCalledWithArguments("expand=groups(offset:5,limit:15)"); } [TestMethod] public void Expand_all_the_things() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetTenantAsync()) @@ -138,15 +140,16 @@ public void Expand_all_the_things() .Expand(x => x.GetDirectoryAsync()) .ToList(); - harness.DataStore.Received().GetCollection($"{Url}/{Resource}?expand=tenant,groups(offset:10,limit:20),directory"); + harness.WasCalledWithArguments("expand=tenant,groups(offset:10,limit:20),directory"); } [TestMethod] public void Expand_throws_if_used_on_an_attribute() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); - Should.Throw(() => { + Should.Throw(() => + { harness.Queryable.Expand(x => x.Email).ToList(); }); } @@ -154,7 +157,7 @@ public void Expand_throws_if_used_on_an_attribute() [TestMethod] public void Expand_throws_if_parameters_are_supplied_for_link() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); Should.Throw(() => { @@ -165,7 +168,7 @@ public void Expand_throws_if_parameters_are_supplied_for_link() [TestMethod] public void Expand_throws_if_syntax_is_dumb() { - var harness = TestHarness.Create(Url, Resource); + var harness = TestHarness.Create(url, resource); Should.Throw(() => { @@ -173,5 +176,693 @@ public void Expand_throws_if_syntax_is_dumb() }); } } + + [TestClass] + public class Where + { + [TestMethod] + public void Where_throws_for_constant() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => true).ToList(); + }); + + Should.Throw(() => + { + harness.Queryable.Where(x => false).ToList(); + }); + } + + [TestMethod] + public void Where_throws_for_unsupported_comparison_operators() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email != "foo").ToList(); + }); + } + + [TestMethod] + public void Where_throws_for_more_complex_overloads_of_helper_methods() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email.Equals("bar", StringComparison.CurrentCulture)).ToList(); + }); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email.StartsWith("foo", StringComparison.OrdinalIgnoreCase)).ToList(); + }); + } + + [TestMethod] + public void Where_throws_for_unsupported_helper_methods() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email.ToUpper() == "FOO").ToList(); + }); + } + + [TestMethod] + public void Where_throws_for_binary_or() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email == "foo" || x.Email == "bar").ToList(); + }); + } + + [TestMethod] + public void Where_attribute_equals() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email == "tk421@deathstar.co") + .ToList(); + + harness.WasCalledWithArguments("email=tk421@deathstar.co"); + } + + [TestMethod] + public void Where_attribute_equals_using_helper_method() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email.Equals("tk421@deathstar.co")) + .ToList(); + + harness.WasCalledWithArguments("email=tk421@deathstar.co"); + } + + [TestMethod] + public void Where_attribute_starts_with() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email.StartsWith("tk421")) + .ToList(); + + harness.WasCalledWithArguments("email=tk421*"); + } + + [TestMethod] + public void Where_attribute_ends_with() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email.EndsWith("deathstar.co")) + .ToList(); + + harness.WasCalledWithArguments("email=*deathstar.co"); + } + + [TestMethod] + public void Where_attribute_contains() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email.Contains("421")) + .ToList(); + + harness.WasCalledWithArguments("email=*421*"); + } + + [TestMethod] + public void Where_multiple_attributes_with_and() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email == "tk421@deathstar.co" && x.Username == "tk421") + .ToList(); + + harness.WasCalledWithArguments("email=tk421@deathstar.co&username=tk421"); + } + + [TestMethod] + public void Where_multiple_wheres() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email == "tk421@deathstar.co") + .Where(x => x.Username.StartsWith("tk421")) + .ToList(); + + harness.WasCalledWithArguments("email=tk421@deathstar.co&username=tk421*"); + } + + [TestMethod] + public void Where_date_attribute_greater_than() + { + var harness = TestHarness.Create(url, resource); + + var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); + harness.Queryable + .Where(x => x.CreatedAt > testDate) + .ToList(); + + harness.WasCalledWithArguments("createdAt=(2015-01-01T06:00:00.000Z,]"); + } + + [TestMethod] + public void Where_date_attribute_greater_than_or_equalto() + { + var harness = TestHarness.Create(url, resource); + + var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); + harness.Queryable + .Where(x => x.CreatedAt >= testDate) + .ToList(); + + harness.WasCalledWithArguments("createdAt=[2015-01-01T06:00:00.000Z,]"); + } + + [TestMethod] + public void Where_date_attribute_less_than() + { + var harness = TestHarness.Create(url, resource); + + var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); + harness.Queryable + .Where(x => x.ModifiedAt < testDate) + .ToList(); + + harness.WasCalledWithArguments("modifiedAt=[,2016-01-01T12:00:00.000Z)"); + } + + [TestMethod] + public void Where_date_attribute_less_than_or_equalto() + { + var harness = TestHarness.Create(url, resource); + + var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); + harness.Queryable + .Where(x => x.ModifiedAt <= testDate) + .ToList(); + + harness.WasCalledWithArguments("modifiedAt=[,2016-01-01T12:00:00.000Z]"); + } + + [TestMethod] + public void Where_date_attribute_between() + { + var harness = TestHarness.Create(url, resource); + + var testStartDate = new DateTimeOffset(2015, 01, 01, 00, 00, 00, TimeSpan.Zero); + var testEndDate = new DateTimeOffset(2015, 12, 31, 23, 59, 59, TimeSpan.Zero); + harness.Queryable + .Where(x => x.CreatedAt > testStartDate && x.CreatedAt <= testEndDate) + .ToList(); + + harness.WasCalledWithArguments("createdAt=(2015-01-01T00:00:00.000Z,2015-12-31T23:59:59.000Z]"); + } + + [TestMethod] + public void Where_date_using_shorthand() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.CreatedAt == Shorthand.Year(2015)) + .ToList(); + + harness.WasCalledWithArguments("createdAt=2015"); + } + } + + [TestClass] + public class Limit + { + [TestMethod] + public void Take_with_constant_becomes_limit() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Take(10) + .ToList(); + + harness.WasCalledWithArguments("limit=10"); + } + + [TestMethod] + public void Take_with_variable_becomes_limit() + { + var harness = TestHarness.Create(url, resource); + + var limit = 20; + harness.Queryable + .Take(limit) + .ToList(); + + harness.WasCalledWithArguments("limit=20"); + } + + [TestMethod] + public void Take_with_function_becomes_limit() + { + var harness = TestHarness.Create(url, resource); + + var limitFunc = new Func(() => 25); + harness.Queryable + .Take(limitFunc()) + .ToList(); + + harness.WasCalledWithArguments("limit=25"); + } + + [TestMethod] + public void Take_multiple_calls_are_LIFO() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Take(10).Take(5) + .ToList(); + + // Expected behavior: the last call will be kept + harness.WasCalledWithArguments("limit=5"); + } + } + + [TestClass] + public class Offset + { + [TestMethod] + public void Skip_becomes_offset() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Skip(10) + .ToList(); + + harness.WasCalledWithArguments("offset=10"); + } + + [TestMethod] + public void Skip_with_variable_becomes_offset() + { + var harness = TestHarness.Create(url, resource); + + var offset = 20; + harness.Queryable + .Skip(offset) + .ToList(); + + harness.WasCalledWithArguments("offset=20"); + } + + [TestMethod] + public void Skip_with_function_becomes_offset() + { + var harness = TestHarness.Create(url, resource); + + var offsetFunc = new Func(() => 25); + harness.Queryable + .Skip(offsetFunc()) + .ToList(); + + harness.WasCalledWithArguments("offset=25"); + } + + [TestMethod] + public void Skip_multiple_calls_are_LIFO() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Skip(10).Skip(5) + .ToList(); + + // Expected behavior: the last call will be kept + harness.WasCalledWithArguments("offset=5"); + } + } + + [TestClass] + public class OrderBy + { + [TestMethod] + public void Order_by_a_field() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .OrderBy(x => x.GivenName) + .ToList(); + + harness.WasCalledWithArguments("orderBy=givenName"); + } + + [TestMethod] + public void Order_by_a_field_descending() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .OrderByDescending(x => x.Email) + .ToList(); + + harness.WasCalledWithArguments("orderBy=email desc"); + } + + [TestMethod] + public void Order_by_multiple_fields() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .OrderBy(x => x.GivenName) + .ThenByDescending(x => x.Username) + .ToList(); + + harness.WasCalledWithArguments("orderBy=givenName,username desc"); + } + + [TestMethod] + public void Order_throws_for_complex_overloads() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.OrderBy(x => x.GivenName, Substitute.For>()).ToList(); + }); + } + } + + [TestClass] + public class UnsupportedFilters + { + [TestMethod] + public void Aggregate_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Aggregate((x, y) => x); + }); + } + + [TestMethod] + public void All_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.All(x => x.Email == "foo"); + }); + } + + [TestMethod] + public void Average_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Average(x => 1.0); + }); + } + + [TestMethod] + public void Cast_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Cast().ToList(); + }); + } + + [TestMethod] + public void Concat_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Concat(Enumerable.Empty()).ToList(); + }); + } + + [TestMethod] + public void Contains_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Contains(Substitute.For()); + }); + } + + [TestMethod] + public void Distinct_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Distinct().ToList(); + }); + } + + [TestMethod] + public void Except_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Except(Enumerable.Empty()).ToList(); + }); + } + + [TestMethod] + public void GroupBy_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.GroupBy(x => x.Email).ToList(); + }); + } + + [TestMethod] + public void GroupJoin_clause_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.GroupJoin(Enumerable.Empty(), + outer => outer.Email, + inner => inner.Username, + (outer, results) => new { outer.CreatedAt, results }) + .ToList(); + }); + } + + [TestMethod] + public void Intersect_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Intersect(Enumerable.Empty()).ToList(); + }); + } + + [TestMethod] + public void Join_clause_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Join(Enumerable.Empty(), + outer => outer.Email, + inner => inner.Username, + (outer, inner) => outer.CreatedAt).ToList(); + }); + } + + [TestMethod] + public void Last_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Last(); + }); + + Should.Throw(() => + { + harness.Queryable.LastOrDefault(); + }); + } + + [TestMethod] + public void Max_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Max(); + }); + } + + [TestMethod] + public void Min_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Min(); + }); + } + + [TestMethod] + public void OfType_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.OfType().ToList(); + }); + } + + [TestMethod] + public void Reverse_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Reverse().ToList(); + }); + } + + [TestMethod] + public void Select_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable + .Select(x => x.Email) + .ToList(); + }); + } + + [TestMethod] + public void SelectMany_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.SelectMany(x => x.Email).ToList(); + }); + } + + [TestMethod] + public void SequenceEqual_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.SequenceEqual(Enumerable.Empty()); + }); + } + + [TestMethod] + public void SkipWhile_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.SkipWhile(x => x.Email == "foobar").ToList(); + }); + } + + [TestMethod] + public void Sum_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Sum(x => 1); + }); + } + + [TestMethod] + public void TakeWhile_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.TakeWhile(x => x.Email == "foobar").ToList(); + }); + } + + [TestMethod] + public void Union_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Union(Enumerable.Empty()).ToList(); + }); + } + + [TestMethod] + public void Zip_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Zip(Enumerable.Empty(), + (first, second) => first.Email == second.Email).ToList(); + }); + } + } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/packages.config b/Stormpath.SDK/Stormpath.SDK.Tests/packages.config index 1ca35d01..232a0064 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/packages.config +++ b/Stormpath.SDK/Stormpath.SDK.Tests/packages.config @@ -1,6 +1,7 @@  + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceWhereExpressionVisitor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceWhereExpressionVisitor.cs index a7579e77..fa4943b5 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceWhereExpressionVisitor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceWhereExpressionVisitor.cs @@ -45,6 +45,7 @@ public override Expression VisitExpression(Expression expression) if (inProgress.IsStringTermComplete()) { this.GeneratedModels.Add(new StringAttributeTermModel(inProgress.Field, inProgress.StringValue, inProgress.StringMatchType.Value)); + inProgress = new WhereAttributeTermInProgressModel(); } if (inProgress.IsDateTermComplete()) @@ -66,6 +67,7 @@ public override Expression VisitExpression(Expression expression) } this.GeneratedModels.Add(model); + inProgress = new WhereAttributeTermInProgressModel(); } return visited; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs index e765b334..06ecc2d2 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs @@ -27,7 +27,6 @@ namespace Stormpath.SDK.Impl.Resource { internal class CollectionResourceQueryable : QueryableBase, ICollectionResourceQueryable - where T : IResource { public CollectionResourceQueryable(string url, string resource, IDataStore dataStore) : base(ExtendedQueryParser.Create(), CreateQueryExecutor(url, resource, dataStore)) @@ -43,11 +42,11 @@ public CollectionResourceQueryable(IQueryProvider provider, Expression expressio { } - public string Url { get; private set; } + internal string Url { get; private set; } - public string Resource { get; private set; } + internal string Resource { get; private set; } - public IDataStore DataStore { get; private set; } + internal IDataStore DataStore { get; private set; } int ICollectionResourceQueryable.Offset { diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs b/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs new file mode 100644 index 00000000..3e2cd6c3 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs @@ -0,0 +1,175 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Stormpath.SDK.Resource +{ + public struct Shorthand + { + private int year; + private int? month; + private int? day; + private int? hour; + private int? minute; + private int? second; + private TimeSpan offset; + + private Shorthand(int year, TimeSpan offset) + { + this.year = year; + this.month = null; + this.day = null; + this.hour = null; + this.minute = null; + this.second = null; + this.offset = offset; + } + + private Shorthand(int year, int month, TimeSpan offset) + { + this.year = year; + this.month = month; + this.day = null; + this.hour = null; + this.minute = null; + this.second = null; + this.offset = offset; + } + + private Shorthand(int year, int month, int day, TimeSpan offset) + { + this.year = year; + this.month = month; + this.day = day; + this.hour = null; + this.minute = null; + this.second = null; + this.offset = offset; + } + + private Shorthand(int year, int month, int day, int hour, TimeSpan offset) + { + this.year = year; + this.month = month; + this.day = day; + this.hour = hour; + this.minute = null; + this.second = null; + this.offset = offset; + } + + private Shorthand(int year, int month, int day, int hour, int minute, TimeSpan offset) + { + this.year = year; + this.month = month; + this.day = day; + this.hour = hour; + this.minute = minute; + this.second = null; + this.offset = offset; + } + + private Shorthand(int year, int month, int day, int hour, int minute, int second, TimeSpan offset) + { + this.year = year; + this.month = month; + this.day = day; + this.hour = hour; + this.minute = minute; + this.second = second; + this.offset = offset; + } + + public static implicit operator DateTimeOffset(Shorthand shorthand) + { + return new DateTimeOffset( + shorthand.year, + shorthand.month ?? 0, + shorthand.day ?? 0, + shorthand.hour ?? 0, + shorthand.minute ?? 0, + shorthand.second ?? 0, + shorthand.offset); + } + + private static TimeSpan GetLocalOffset() + { + return TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow); + } + + public static Shorthand Year(int year) + { + return Year(year, GetLocalOffset()); + } + + public static Shorthand Year(int year, TimeSpan offset) + { + return new Shorthand(year, offset); + } + + public static Shorthand Month(int year, int month) + { + return Month(year, month, GetLocalOffset()); + } + + public static Shorthand Month(int year, int month, TimeSpan offset) + { + return new Shorthand(year, month, offset); + } + + public static Shorthand Day(int year, int month, int day) + { + return Day(year, month, day, GetLocalOffset()); + } + + public static Shorthand Day(int year, int month, int day, TimeSpan offset) + { + return new Shorthand(year, month, day, offset); + } + + public static Shorthand Hour(int year, int month, int day, int hour) + { + return Hour(year, month, day, hour, GetLocalOffset()); + } + + public static Shorthand Hour(int year, int month, int day, int hour, TimeSpan offset) + { + return new Shorthand(year, month, day, hour, offset); + } + + public static Shorthand Minute(int year, int month, int day, int hour, int minute) + { + return Minute(year, month, day, hour, minute, GetLocalOffset()); + } + + public static Shorthand Minute(int year, int month, int day, int hour, int minute, TimeSpan offset) + { + return new Shorthand(year, month, day, hour, minute, offset); + } + + public static Shorthand Second(int year, int month, int day, int hour, int minute, int second) + { + return Second(year, month, day, hour, minute, second, GetLocalOffset()); + } + + public static Shorthand Second(int year, int month, int day, int hour, int minute, int second, TimeSpan offset) + { + return new Shorthand(year, month, day, hour, minute, second, offset); + } + } +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 7fade7a4..383dde89 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -107,6 +107,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs index 533ca45b..ce0a5d16 100644 --- a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs +++ b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs @@ -16,10 +16,11 @@ // using System.Threading.Tasks; +using Stormpath.SDK.Resource; namespace Stormpath.SDK.Tenant { - public interface ITenant : ITenantActions + public interface ITenant : IResource, ITenantActions { Task GetNameAsync(); From 1e9cc75ec39135e9353910bc944b5f0d72f0fb4e Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 10 Aug 2015 10:16:09 -0700 Subject: [PATCH 025/238] Minor changes to tests and demo code --- Stormpath.SDK/Stormpath.Demo/Program.cs | 17 ++++++++++++----- .../Impl/Linq/TestHarness.cs | 5 +++++ .../Application/ApplicationStatus.cs | 14 ++++++++++++++ .../Stormpath.SDK/Application/IApplication.cs | 3 +++ .../Resource/ICollectionResourceQueryable.cs | 1 - .../Stormpath.SDK/Resource/Shorthand.cs | 4 ++-- .../Stormpath.SDK/Stormpath.SDK.csproj | 1 + Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs | 4 +--- .../Stormpath.SDK/Tenant/ITenantActions.cs | 2 ++ 9 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs diff --git a/Stormpath.SDK/Stormpath.Demo/Program.cs b/Stormpath.SDK/Stormpath.Demo/Program.cs index a21961f4..8ea141e8 100644 --- a/Stormpath.SDK/Stormpath.Demo/Program.cs +++ b/Stormpath.SDK/Stormpath.Demo/Program.cs @@ -1,8 +1,10 @@ using System; using System.Threading.Tasks; +using System.Linq; using Stormpath.SDK.Api; using Stormpath.SDK.Client; using System.Threading; +using Stormpath.SDK.Application; namespace Stormpath.Demo { @@ -42,15 +44,20 @@ static async Task MainAsync(CancellationToken ct) // Get current tenant var tenant = await client.GetCurrentTenantAsync(); - Console.WriteLine($"Current tenant is: {await tenant.GetNameAsync()}"); + Console.WriteLine($"Current tenant is: {tenant.Name}"); + + // Get + var myApp = tenant.GetApplications() + .Where(x => x.Name == "My Application") + .First(); // List applications - // TODO - //Console.WriteLine("\nTenant applications:"); + //Console.WriteLine("Tenant applications:"); //var applications = await tenant.GetApplicationsAsync(); //foreach (var app in applications) - // Console.WriteLine("{0}\t{1}", app.Name, app.Status ? "enabled" : "disabled"); - //var myApp = applications.First(); + //{ + // Console.WriteLine("{0}\t{1}", app.Name, app.Status == ApplicationStatus.Enabled ? "enabled" : "disabled"); + //} //// Add some users //Console.WriteLine("\nAdding users to '{0}'...", myApp.Name); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs index fc2f2a15..a3f24187 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs @@ -33,6 +33,11 @@ public class TestHarness public ICollectionResourceQueryable Queryable { get; private set; } + public void WasCalledWithArguments(string arguments) + { + DataStore.Received().GetCollection($"{Url}/{Resource}?{arguments}"); + } + public static TestHarness Create(string url, string resource) where TType : IResource { diff --git a/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs b/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs new file mode 100644 index 00000000..fddddce2 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Application +{ + public enum ApplicationStatus + { + Disabled = 0, + Enabled = 1 + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Application/IApplication.cs b/Stormpath.SDK/Stormpath.SDK/Application/IApplication.cs index cd8a420e..f5682c59 100644 --- a/Stormpath.SDK/Stormpath.SDK/Application/IApplication.cs +++ b/Stormpath.SDK/Stormpath.SDK/Application/IApplication.cs @@ -21,5 +21,8 @@ namespace Stormpath.SDK.Application { public interface IApplication : IResource { + string Name { get; } + + ApplicationStatus Status { get; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs index a9b49c78..b8b14969 100644 --- a/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs +++ b/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs @@ -20,7 +20,6 @@ namespace Stormpath.SDK.Resource { public interface ICollectionResourceQueryable : IQueryable - where T : IResource { int Offset { get; } diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs b/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs index 3e2cd6c3..d12058df 100644 --- a/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs +++ b/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs @@ -99,8 +99,8 @@ public static implicit operator DateTimeOffset(Shorthand shorthand) { return new DateTimeOffset( shorthand.year, - shorthand.month ?? 0, - shorthand.day ?? 0, + shorthand.month ?? 1, + shorthand.day ?? 1, shorthand.hour ?? 0, shorthand.minute ?? 0, shorthand.second ?? 0, diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 383dde89..be05fcaf 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -57,6 +57,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs index ce0a5d16..61f8da3a 100644 --- a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs +++ b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs @@ -22,8 +22,6 @@ namespace Stormpath.SDK.Tenant { public interface ITenant : IResource, ITenantActions { - Task GetNameAsync(); - - Task GetKeyAsync(); + string Name { get; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs index 4e2a403e..7504234d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs +++ b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs @@ -29,6 +29,8 @@ public interface ITenantActions Task GetApplicationsAsync(); + IApplicationList GetApplications(); + Task CreateDirectoryAsync(IDirectory directory); Task GetDirectoriesAsync(); From 46174e59f0a129409714e7f815ba4dcbb226ce91 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 10 Aug 2015 11:27:27 -0700 Subject: [PATCH 026/238] Split up LINQ tests into separate files --- .../Impl/Linq/ExpandExtensionTests.cs | 140 +++ .../Impl/Linq/FilterExtensionTests.cs | 59 ++ .../Impl/Linq/LimitTests.cs | 82 ++ .../Impl/Linq/LinqTests.cs | 868 ------------------ .../Impl/Linq/OffsetTests.cs | 59 ++ .../Impl/Linq/OrderByTests.cs | 82 ++ .../Impl/Linq/UnsupportedFilterTests.cs | 323 +++++++ .../Impl/Linq/WhereTests.cs | 261 ++++++ .../Stormpath.SDK.Tests.csproj | 8 +- 9 files changed, 1013 insertions(+), 869 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs delete mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs new file mode 100644 index 00000000..eeae6f68 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stormpath.SDK.Account; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class ExpandExtensionTests + { + private static string url = "http://f.oo"; + private static string resource = "bar"; + + [TestMethod] + public void Expand_one_link() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Expand(x => x.GetDirectoryAsync()) + .ToList(); + + harness.WasCalledWithArguments("expand=directory"); + } + + [TestMethod] + public void Expand_multiple_links() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Expand(x => x.GetDirectoryAsync()) + .Expand(x => x.GetTenantAsync()) + .ToList(); + + harness.WasCalledWithArguments("expand=directory,tenant"); + } + + [TestMethod] + public void Expand_collection_query_with_offset() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Expand(x => x.GetGroupsAsync(), offset: 10) + .ToList(); + + harness.WasCalledWithArguments("expand=groups(offset:10)"); + } + + [TestMethod] + public void Expand_collection_query_with_limit() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Expand(x => x.GetGroupsAsync(), limit: 20) + .ToList(); + + harness.WasCalledWithArguments("expand=groups(limit:20)"); + } + + [TestMethod] + public void Expand_collection_query_with_both_parameters() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Expand(x => x.GetGroupsAsync(), 5, 15) + .ToList(); + + harness.WasCalledWithArguments("expand=groups(offset:5,limit:15)"); + } + + [TestMethod] + public void Expand_all_the_things() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Expand(x => x.GetTenantAsync()) + .Expand(x => x.GetGroupsAsync(), 10, 20) + .Expand(x => x.GetDirectoryAsync()) + .ToList(); + + harness.WasCalledWithArguments("expand=tenant,groups(offset:10,limit:20),directory"); + } + + [TestMethod] + public void Expand_throws_if_used_on_an_attribute() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Expand(x => x.Email).ToList(); + }); + } + + [TestMethod] + public void Expand_throws_if_parameters_are_supplied_for_link() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Expand(x => x.GetDirectoryAsync(), limit: 10).ToList(); + }); + } + + [TestMethod] + public void Expand_throws_if_syntax_is_dumb() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Expand(x => x.GetTenantAsync().GetAwaiter()).ToList(); + }); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs new file mode 100644 index 00000000..f7370463 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Stormpath.SDK.Account; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class FilterExtensionTests + { + private static string url = "http://f.oo"; + private static string resource = "bar"; + + [TestMethod] + public void Filter_with_simple_parameter() + { + // Arrange + var harness = TestHarness.Create(url, resource); + + // Act + harness.Queryable + .Filter("Joe") + .ToList(); + + // Assert + harness.WasCalledWithArguments("q=Joe"); + } + + [TestMethod] + public void Filter_multiple_calls_are_LIFO() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Filter("Joe") + .Filter("Joey") + .ToList(); + + // Expected behavior: the last call will be kept + harness.WasCalledWithArguments("q=Joey"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs new file mode 100644 index 00000000..51c33c9e --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Stormpath.SDK.Account; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class LimitTests + { + private static string url = "http://f.oo"; + private static string resource = "bar"; + + [TestMethod] + public void Take_with_constant_becomes_limit() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Take(10) + .ToList(); + + harness.WasCalledWithArguments("limit=10"); + } + + [TestMethod] + public void Take_with_variable_becomes_limit() + { + var harness = TestHarness.Create(url, resource); + + var limit = 20; + harness.Queryable + .Take(limit) + .ToList(); + + harness.WasCalledWithArguments("limit=20"); + } + + [TestMethod] + public void Take_with_function_becomes_limit() + { + var harness = TestHarness.Create(url, resource); + + var limitFunc = new Func(() => 25); + harness.Queryable + .Take(limitFunc()) + .ToList(); + + harness.WasCalledWithArguments("limit=25"); + } + + [TestMethod] + public void Take_multiple_calls_are_LIFO() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Take(10).Take(5) + .ToList(); + + // Expected behavior: the last call will be kept + harness.WasCalledWithArguments("limit=5"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs deleted file mode 100644 index 304e2d97..00000000 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTests.cs +++ /dev/null @@ -1,868 +0,0 @@ -// -// Copyright (c) 2015 Stormpath, Inc. -// -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using NSubstitute; -using Shouldly; -using Stormpath.SDK.Account; -using Stormpath.SDK.Resource; - -namespace Stormpath.SDK.Tests.Impl.Linq -{ - [TestClass] - public class LinqTests - { - private static string url = "http://f.oo"; - private static string resource = "bar"; - - [TestClass] - public class FilterExtensionMethod - { - [TestMethod] - public void Filter_with_simple_parameter() - { - // Arrange - var harness = TestHarness.Create(url, resource); - - // Act - harness.Queryable - .Filter("Joe") - .ToList(); - - // Assert - harness.WasCalledWithArguments("q=Joe"); - } - - [TestMethod] - public void Filter_multiple_calls_are_LIFO() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Filter("Joe") - .Filter("Joey") - .ToList(); - - // Expected behavior: the last call will be kept - harness.WasCalledWithArguments("q=Joey"); - } - } - - [TestClass] - public class ExpandExtensionMethod - { - [TestMethod] - public void Expand_one_link() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Expand(x => x.GetDirectoryAsync()) - .ToList(); - - harness.WasCalledWithArguments("expand=directory"); - } - - [TestMethod] - public void Expand_multiple_links() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Expand(x => x.GetDirectoryAsync()) - .Expand(x => x.GetTenantAsync()) - .ToList(); - - harness.WasCalledWithArguments("expand=directory,tenant"); - } - - [TestMethod] - public void Expand_collection_query_with_offset() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Expand(x => x.GetGroupsAsync(), offset: 10) - .ToList(); - - harness.WasCalledWithArguments("expand=groups(offset:10)"); - } - - [TestMethod] - public void Expand_collection_query_with_limit() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Expand(x => x.GetGroupsAsync(), limit: 20) - .ToList(); - - harness.WasCalledWithArguments("expand=groups(limit:20)"); - } - - [TestMethod] - public void Expand_collection_query_with_both_parameters() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Expand(x => x.GetGroupsAsync(), 5, 15) - .ToList(); - - harness.WasCalledWithArguments("expand=groups(offset:5,limit:15)"); - } - - [TestMethod] - public void Expand_all_the_things() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Expand(x => x.GetTenantAsync()) - .Expand(x => x.GetGroupsAsync(), 10, 20) - .Expand(x => x.GetDirectoryAsync()) - .ToList(); - - harness.WasCalledWithArguments("expand=tenant,groups(offset:10,limit:20),directory"); - } - - [TestMethod] - public void Expand_throws_if_used_on_an_attribute() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Expand(x => x.Email).ToList(); - }); - } - - [TestMethod] - public void Expand_throws_if_parameters_are_supplied_for_link() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Expand(x => x.GetDirectoryAsync(), limit: 10).ToList(); - }); - } - - [TestMethod] - public void Expand_throws_if_syntax_is_dumb() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Expand(x => x.GetTenantAsync().GetAwaiter()).ToList(); - }); - } - } - - [TestClass] - public class Where - { - [TestMethod] - public void Where_throws_for_constant() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Where(x => true).ToList(); - }); - - Should.Throw(() => - { - harness.Queryable.Where(x => false).ToList(); - }); - } - - [TestMethod] - public void Where_throws_for_unsupported_comparison_operators() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Where(x => x.Email != "foo").ToList(); - }); - } - - [TestMethod] - public void Where_throws_for_more_complex_overloads_of_helper_methods() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Where(x => x.Email.Equals("bar", StringComparison.CurrentCulture)).ToList(); - }); - - Should.Throw(() => - { - harness.Queryable.Where(x => x.Email.StartsWith("foo", StringComparison.OrdinalIgnoreCase)).ToList(); - }); - } - - [TestMethod] - public void Where_throws_for_unsupported_helper_methods() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Where(x => x.Email.ToUpper() == "FOO").ToList(); - }); - } - - [TestMethod] - public void Where_throws_for_binary_or() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Where(x => x.Email == "foo" || x.Email == "bar").ToList(); - }); - } - - [TestMethod] - public void Where_attribute_equals() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Where(x => x.Email == "tk421@deathstar.co") - .ToList(); - - harness.WasCalledWithArguments("email=tk421@deathstar.co"); - } - - [TestMethod] - public void Where_attribute_equals_using_helper_method() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Where(x => x.Email.Equals("tk421@deathstar.co")) - .ToList(); - - harness.WasCalledWithArguments("email=tk421@deathstar.co"); - } - - [TestMethod] - public void Where_attribute_starts_with() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Where(x => x.Email.StartsWith("tk421")) - .ToList(); - - harness.WasCalledWithArguments("email=tk421*"); - } - - [TestMethod] - public void Where_attribute_ends_with() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Where(x => x.Email.EndsWith("deathstar.co")) - .ToList(); - - harness.WasCalledWithArguments("email=*deathstar.co"); - } - - [TestMethod] - public void Where_attribute_contains() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Where(x => x.Email.Contains("421")) - .ToList(); - - harness.WasCalledWithArguments("email=*421*"); - } - - [TestMethod] - public void Where_multiple_attributes_with_and() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Where(x => x.Email == "tk421@deathstar.co" && x.Username == "tk421") - .ToList(); - - harness.WasCalledWithArguments("email=tk421@deathstar.co&username=tk421"); - } - - [TestMethod] - public void Where_multiple_wheres() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Where(x => x.Email == "tk421@deathstar.co") - .Where(x => x.Username.StartsWith("tk421")) - .ToList(); - - harness.WasCalledWithArguments("email=tk421@deathstar.co&username=tk421*"); - } - - [TestMethod] - public void Where_date_attribute_greater_than() - { - var harness = TestHarness.Create(url, resource); - - var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); - harness.Queryable - .Where(x => x.CreatedAt > testDate) - .ToList(); - - harness.WasCalledWithArguments("createdAt=(2015-01-01T06:00:00.000Z,]"); - } - - [TestMethod] - public void Where_date_attribute_greater_than_or_equalto() - { - var harness = TestHarness.Create(url, resource); - - var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); - harness.Queryable - .Where(x => x.CreatedAt >= testDate) - .ToList(); - - harness.WasCalledWithArguments("createdAt=[2015-01-01T06:00:00.000Z,]"); - } - - [TestMethod] - public void Where_date_attribute_less_than() - { - var harness = TestHarness.Create(url, resource); - - var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); - harness.Queryable - .Where(x => x.ModifiedAt < testDate) - .ToList(); - - harness.WasCalledWithArguments("modifiedAt=[,2016-01-01T12:00:00.000Z)"); - } - - [TestMethod] - public void Where_date_attribute_less_than_or_equalto() - { - var harness = TestHarness.Create(url, resource); - - var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); - harness.Queryable - .Where(x => x.ModifiedAt <= testDate) - .ToList(); - - harness.WasCalledWithArguments("modifiedAt=[,2016-01-01T12:00:00.000Z]"); - } - - [TestMethod] - public void Where_date_attribute_between() - { - var harness = TestHarness.Create(url, resource); - - var testStartDate = new DateTimeOffset(2015, 01, 01, 00, 00, 00, TimeSpan.Zero); - var testEndDate = new DateTimeOffset(2015, 12, 31, 23, 59, 59, TimeSpan.Zero); - harness.Queryable - .Where(x => x.CreatedAt > testStartDate && x.CreatedAt <= testEndDate) - .ToList(); - - harness.WasCalledWithArguments("createdAt=(2015-01-01T00:00:00.000Z,2015-12-31T23:59:59.000Z]"); - } - - [TestMethod] - public void Where_date_using_shorthand() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Where(x => x.CreatedAt == Shorthand.Year(2015)) - .ToList(); - - harness.WasCalledWithArguments("createdAt=2015"); - } - } - - [TestClass] - public class Limit - { - [TestMethod] - public void Take_with_constant_becomes_limit() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Take(10) - .ToList(); - - harness.WasCalledWithArguments("limit=10"); - } - - [TestMethod] - public void Take_with_variable_becomes_limit() - { - var harness = TestHarness.Create(url, resource); - - var limit = 20; - harness.Queryable - .Take(limit) - .ToList(); - - harness.WasCalledWithArguments("limit=20"); - } - - [TestMethod] - public void Take_with_function_becomes_limit() - { - var harness = TestHarness.Create(url, resource); - - var limitFunc = new Func(() => 25); - harness.Queryable - .Take(limitFunc()) - .ToList(); - - harness.WasCalledWithArguments("limit=25"); - } - - [TestMethod] - public void Take_multiple_calls_are_LIFO() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Take(10).Take(5) - .ToList(); - - // Expected behavior: the last call will be kept - harness.WasCalledWithArguments("limit=5"); - } - } - - [TestClass] - public class Offset - { - [TestMethod] - public void Skip_becomes_offset() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Skip(10) - .ToList(); - - harness.WasCalledWithArguments("offset=10"); - } - - [TestMethod] - public void Skip_with_variable_becomes_offset() - { - var harness = TestHarness.Create(url, resource); - - var offset = 20; - harness.Queryable - .Skip(offset) - .ToList(); - - harness.WasCalledWithArguments("offset=20"); - } - - [TestMethod] - public void Skip_with_function_becomes_offset() - { - var harness = TestHarness.Create(url, resource); - - var offsetFunc = new Func(() => 25); - harness.Queryable - .Skip(offsetFunc()) - .ToList(); - - harness.WasCalledWithArguments("offset=25"); - } - - [TestMethod] - public void Skip_multiple_calls_are_LIFO() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Skip(10).Skip(5) - .ToList(); - - // Expected behavior: the last call will be kept - harness.WasCalledWithArguments("offset=5"); - } - } - - [TestClass] - public class OrderBy - { - [TestMethod] - public void Order_by_a_field() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .OrderBy(x => x.GivenName) - .ToList(); - - harness.WasCalledWithArguments("orderBy=givenName"); - } - - [TestMethod] - public void Order_by_a_field_descending() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .OrderByDescending(x => x.Email) - .ToList(); - - harness.WasCalledWithArguments("orderBy=email desc"); - } - - [TestMethod] - public void Order_by_multiple_fields() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .OrderBy(x => x.GivenName) - .ThenByDescending(x => x.Username) - .ToList(); - - harness.WasCalledWithArguments("orderBy=givenName,username desc"); - } - - [TestMethod] - public void Order_throws_for_complex_overloads() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.OrderBy(x => x.GivenName, Substitute.For>()).ToList(); - }); - } - } - - [TestClass] - public class UnsupportedFilters - { - [TestMethod] - public void Aggregate_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Aggregate((x, y) => x); - }); - } - - [TestMethod] - public void All_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.All(x => x.Email == "foo"); - }); - } - - [TestMethod] - public void Average_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Average(x => 1.0); - }); - } - - [TestMethod] - public void Cast_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Cast().ToList(); - }); - } - - [TestMethod] - public void Concat_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Concat(Enumerable.Empty()).ToList(); - }); - } - - [TestMethod] - public void Contains_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Contains(Substitute.For()); - }); - } - - [TestMethod] - public void Distinct_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Distinct().ToList(); - }); - } - - [TestMethod] - public void Except_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Except(Enumerable.Empty()).ToList(); - }); - } - - [TestMethod] - public void GroupBy_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.GroupBy(x => x.Email).ToList(); - }); - } - - [TestMethod] - public void GroupJoin_clause_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.GroupJoin(Enumerable.Empty(), - outer => outer.Email, - inner => inner.Username, - (outer, results) => new { outer.CreatedAt, results }) - .ToList(); - }); - } - - [TestMethod] - public void Intersect_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Intersect(Enumerable.Empty()).ToList(); - }); - } - - [TestMethod] - public void Join_clause_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Join(Enumerable.Empty(), - outer => outer.Email, - inner => inner.Username, - (outer, inner) => outer.CreatedAt).ToList(); - }); - } - - [TestMethod] - public void Last_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Last(); - }); - - Should.Throw(() => - { - harness.Queryable.LastOrDefault(); - }); - } - - [TestMethod] - public void Max_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Max(); - }); - } - - [TestMethod] - public void Min_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Min(); - }); - } - - [TestMethod] - public void OfType_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.OfType().ToList(); - }); - } - - [TestMethod] - public void Reverse_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Reverse().ToList(); - }); - } - - [TestMethod] - public void Select_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable - .Select(x => x.Email) - .ToList(); - }); - } - - [TestMethod] - public void SelectMany_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.SelectMany(x => x.Email).ToList(); - }); - } - - [TestMethod] - public void SequenceEqual_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.SequenceEqual(Enumerable.Empty()); - }); - } - - [TestMethod] - public void SkipWhile_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.SkipWhile(x => x.Email == "foobar").ToList(); - }); - } - - [TestMethod] - public void Sum_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Sum(x => 1); - }); - } - - [TestMethod] - public void TakeWhile_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.TakeWhile(x => x.Email == "foobar").ToList(); - }); - } - - [TestMethod] - public void Union_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Union(Enumerable.Empty()).ToList(); - }); - } - - [TestMethod] - public void Zip_is_unsupported() - { - var harness = TestHarness.Create(url, resource); - - Should.Throw(() => - { - harness.Queryable.Zip(Enumerable.Empty(), - (first, second) => first.Email == second.Email).ToList(); - }); - } - } - } -} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs new file mode 100644 index 00000000..194343a8 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Stormpath.SDK.Account; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class OffsetTests + { + private static string url = "http://f.oo"; + private static string resource = "bar"; + + [TestMethod] + public void Filter_with_simple_parameter() + { + // Arrange + var harness = TestHarness.Create(url, resource); + + // Act + harness.Queryable + .Filter("Joe") + .ToList(); + + // Assert + harness.WasCalledWithArguments("q=Joe"); + } + + [TestMethod] + public void Filter_multiple_calls_are_LIFO() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Filter("Joe") + .Filter("Joey") + .ToList(); + + // Expected behavior: the last call will be kept + harness.WasCalledWithArguments("q=Joey"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs new file mode 100644 index 00000000..8f582870 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Stormpath.SDK.Account; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class OrderByTests + { + private static string url = "http://f.oo"; + private static string resource = "bar"; + + [TestMethod] + public void Skip_becomes_offset() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Skip(10) + .ToList(); + + harness.WasCalledWithArguments("offset=10"); + } + + [TestMethod] + public void Skip_with_variable_becomes_offset() + { + var harness = TestHarness.Create(url, resource); + + var offset = 20; + harness.Queryable + .Skip(offset) + .ToList(); + + harness.WasCalledWithArguments("offset=20"); + } + + [TestMethod] + public void Skip_with_function_becomes_offset() + { + var harness = TestHarness.Create(url, resource); + + var offsetFunc = new Func(() => 25); + harness.Queryable + .Skip(offsetFunc()) + .ToList(); + + harness.WasCalledWithArguments("offset=25"); + } + + [TestMethod] + public void Skip_multiple_calls_are_LIFO() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Skip(10).Skip(5) + .ToList(); + + // Expected behavior: the last call will be kept + harness.WasCalledWithArguments("offset=5"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs new file mode 100644 index 00000000..ca027eed --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs @@ -0,0 +1,323 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using Shouldly; +using Stormpath.SDK.Account; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class UnsupportedFilterTests + { + private static string url = "http://f.oo"; + private static string resource = "bar"; + + [TestMethod] + public void Aggregate_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Aggregate((x, y) => x); + }); + } + + [TestMethod] + public void All_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.All(x => x.Email == "foo"); + }); + } + + [TestMethod] + public void Average_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Average(x => 1.0); + }); + } + + [TestMethod] + public void Cast_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Cast().ToList(); + }); + } + + [TestMethod] + public void Concat_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Concat(Enumerable.Empty()).ToList(); + }); + } + + [TestMethod] + public void Contains_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Contains(Substitute.For()); + }); + } + + [TestMethod] + public void Distinct_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Distinct().ToList(); + }); + } + + [TestMethod] + public void Except_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Except(Enumerable.Empty()).ToList(); + }); + } + + [TestMethod] + public void GroupBy_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.GroupBy(x => x.Email).ToList(); + }); + } + + [TestMethod] + public void GroupJoin_clause_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.GroupJoin(Enumerable.Empty(), + outer => outer.Email, + inner => inner.Username, + (outer, results) => new { outer.CreatedAt, results }) + .ToList(); + }); + } + + [TestMethod] + public void Intersect_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Intersect(Enumerable.Empty()).ToList(); + }); + } + + [TestMethod] + public void Join_clause_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Join(Enumerable.Empty(), + outer => outer.Email, + inner => inner.Username, + (outer, inner) => outer.CreatedAt).ToList(); + }); + } + + [TestMethod] + public void Last_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Last(); + }); + + Should.Throw(() => + { + harness.Queryable.LastOrDefault(); + }); + } + + [TestMethod] + public void Max_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Max(); + }); + } + + [TestMethod] + public void Min_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Min(); + }); + } + + [TestMethod] + public void OfType_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.OfType().ToList(); + }); + } + + [TestMethod] + public void Reverse_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Reverse().ToList(); + }); + } + + [TestMethod] + public void Select_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable + .Select(x => x.Email) + .ToList(); + }); + } + + [TestMethod] + public void SelectMany_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.SelectMany(x => x.Email).ToList(); + }); + } + + [TestMethod] + public void SequenceEqual_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.SequenceEqual(Enumerable.Empty()); + }); + } + + [TestMethod] + public void SkipWhile_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.SkipWhile(x => x.Email == "foobar").ToList(); + }); + } + + [TestMethod] + public void Sum_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Sum(x => 1); + }); + } + + [TestMethod] + public void TakeWhile_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.TakeWhile(x => x.Email == "foobar").ToList(); + }); + } + + [TestMethod] + public void Union_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Union(Enumerable.Empty()).ToList(); + }); + } + + [TestMethod] + public void Zip_is_unsupported() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Zip(Enumerable.Empty(), + (first, second) => first.Email == second.Email).ToList(); + }); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs new file mode 100644 index 00000000..301fc3e4 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs @@ -0,0 +1,261 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stormpath.SDK.Account; +using Stormpath.SDK.Resource; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class WhereTests + { + private static string url = "http://f.oo"; + private static string resource = "bar"; + + [TestMethod] + public void Where_throws_for_constant() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => true).ToList(); + }); + + Should.Throw(() => + { + harness.Queryable.Where(x => false).ToList(); + }); + } + + [TestMethod] + public void Where_throws_for_unsupported_comparison_operators() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email != "foo").ToList(); + }); + } + + [TestMethod] + public void Where_throws_for_more_complex_overloads_of_helper_methods() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email.Equals("bar", StringComparison.CurrentCulture)).ToList(); + }); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email.StartsWith("foo", StringComparison.OrdinalIgnoreCase)).ToList(); + }); + } + + [TestMethod] + public void Where_throws_for_unsupported_helper_methods() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email.ToUpper() == "FOO").ToList(); + }); + } + + [TestMethod] + public void Where_throws_for_binary_or() + { + var harness = TestHarness.Create(url, resource); + + Should.Throw(() => + { + harness.Queryable.Where(x => x.Email == "foo" || x.Email == "bar").ToList(); + }); + } + + [TestMethod] + public void Where_attribute_equals() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email == "tk421@deathstar.co") + .ToList(); + + harness.WasCalledWithArguments("email=tk421@deathstar.co"); + } + + [TestMethod] + public void Where_attribute_equals_using_helper_method() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email.Equals("tk421@deathstar.co")) + .ToList(); + + harness.WasCalledWithArguments("email=tk421@deathstar.co"); + } + + [TestMethod] + public void Where_attribute_starts_with() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email.StartsWith("tk421")) + .ToList(); + + harness.WasCalledWithArguments("email=tk421*"); + } + + [TestMethod] + public void Where_attribute_ends_with() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email.EndsWith("deathstar.co")) + .ToList(); + + harness.WasCalledWithArguments("email=*deathstar.co"); + } + + [TestMethod] + public void Where_attribute_contains() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email.Contains("421")) + .ToList(); + + harness.WasCalledWithArguments("email=*421*"); + } + + [TestMethod] + public void Where_multiple_attributes_with_and() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email == "tk421@deathstar.co" && x.Username == "tk421") + .ToList(); + + harness.WasCalledWithArguments("email=tk421@deathstar.co&username=tk421"); + } + + [TestMethod] + public void Where_multiple_wheres() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.Email == "tk421@deathstar.co") + .Where(x => x.Username.StartsWith("tk421")) + .ToList(); + + harness.WasCalledWithArguments("email=tk421@deathstar.co&username=tk421*"); + } + + [TestMethod] + public void Where_date_attribute_greater_than() + { + var harness = TestHarness.Create(url, resource); + + var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); + harness.Queryable + .Where(x => x.CreatedAt > testDate) + .ToList(); + + harness.WasCalledWithArguments("createdAt=(2015-01-01T06:00:00.000Z,]"); + } + + [TestMethod] + public void Where_date_attribute_greater_than_or_equalto() + { + var harness = TestHarness.Create(url, resource); + + var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); + harness.Queryable + .Where(x => x.CreatedAt >= testDate) + .ToList(); + + harness.WasCalledWithArguments("createdAt=[2015-01-01T06:00:00.000Z,]"); + } + + [TestMethod] + public void Where_date_attribute_less_than() + { + var harness = TestHarness.Create(url, resource); + + var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); + harness.Queryable + .Where(x => x.ModifiedAt < testDate) + .ToList(); + + harness.WasCalledWithArguments("modifiedAt=[,2016-01-01T12:00:00.000Z)"); + } + + [TestMethod] + public void Where_date_attribute_less_than_or_equalto() + { + var harness = TestHarness.Create(url, resource); + + var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); + harness.Queryable + .Where(x => x.ModifiedAt <= testDate) + .ToList(); + + harness.WasCalledWithArguments("modifiedAt=[,2016-01-01T12:00:00.000Z]"); + } + + [TestMethod] + public void Where_date_attribute_between() + { + var harness = TestHarness.Create(url, resource); + + var testStartDate = new DateTimeOffset(2015, 01, 01, 00, 00, 00, TimeSpan.Zero); + var testEndDate = new DateTimeOffset(2015, 12, 31, 23, 59, 59, TimeSpan.Zero); + harness.Queryable + .Where(x => x.CreatedAt > testStartDate && x.CreatedAt <= testEndDate) + .ToList(); + + harness.WasCalledWithArguments("createdAt=(2015-01-01T00:00:00.000Z,2015-12-31T23:59:59.000Z]"); + } + + [TestMethod] + public void Where_date_using_shorthand() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.CreatedAt == Shorthand.Year(2015)) + .ToList(); + + harness.WasCalledWithArguments("createdAt=2015"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index e19aefa1..25f9c5bb 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -70,7 +70,13 @@ - + + + + + + + From 6e9c45729f1f4bc1387bc450cc22d300782136e8 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 10 Aug 2015 15:08:35 -0700 Subject: [PATCH 027/238] Shorthand date support and test cleanup --- Stormpath.SDK/Stormpath.Demo/Program.cs | 1 - .../CollectionResourceRequestModelTests.cs | 7 + .../Impl/Linq/ExpandExtensionTests.cs | 9 + .../Impl/Linq/FilterExtensionTests.cs | 2 + .../Impl/Linq/LimitTests.cs | 4 + .../Impl/Linq/OffsetTests.cs | 2 + .../Impl/Linq/WhereTests.cs | 29 +-- .../Impl/Linq/WithinExtensionTests.cs | 121 +++++++++++ .../Stormpath.SDK.Tests.csproj | 1 + .../Application/ApplicationStatus.cs | 28 ++- .../Stormpath.SDK/DataStore/IDataStore.cs | 1 - .../Impl/Client/DefaultClient.cs | 5 + .../Linq/CollectionResourceQueryExecutor.cs | 1 - .../CollectionResourceQueryModelVisitor.cs | 192 ++++++++++++------ .../CollectionResourceRequestModelCompiler.cs | 51 ++++- .../CollectionResourceRequestModel.cs | 13 +- .../DatetimeShorthandAttributeTermModel.cs | 55 +++++ .../CollectionLinkMethodNameTranslator.cs | 2 +- .../CollectionResourceQueryableExtensions.cs | 36 +++- .../Stormpath.SDK/Stormpath.SDK.csproj | 1 + Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs | 1 - 21 files changed, 472 insertions(+), 90 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeShorthandAttributeTermModel.cs diff --git a/Stormpath.SDK/Stormpath.Demo/Program.cs b/Stormpath.SDK/Stormpath.Demo/Program.cs index 8ea141e8..479489d3 100644 --- a/Stormpath.SDK/Stormpath.Demo/Program.cs +++ b/Stormpath.SDK/Stormpath.Demo/Program.cs @@ -4,7 +4,6 @@ using Stormpath.SDK.Api; using Stormpath.SDK.Client; using System.Threading; -using Stormpath.SDK.Application; namespace Stormpath.Demo { diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/CollectionResourceRequestModelTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/CollectionResourceRequestModelTests.cs index 16454cab..96df6d1e 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/CollectionResourceRequestModelTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/CollectionResourceRequestModelTests.cs @@ -28,6 +28,7 @@ public class CollectionResourceRequestModelTests public class ExpansionTermTests { [TestMethod] + [TestCategory("Impl.Linq")] public void Expansion_with_null_subparameters_is_valid() { var expansion = new ExpansionTerm("foo"); @@ -36,6 +37,7 @@ public void Expansion_with_null_subparameters_is_valid() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expansion_with_subparameters_is_valid() { var expansion = new ExpansionTerm("foo", 10, 10); @@ -44,6 +46,7 @@ public void Expansion_with_subparameters_is_valid() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expansion_with_empty_fieldname_is_invalid() { var expansion = new ExpansionTerm(string.Empty); @@ -52,6 +55,7 @@ public void Expansion_with_empty_fieldname_is_invalid() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expansion_with_offset_zero_is_valid() { var expansion = new ExpansionTerm("foo", offset: 0); @@ -60,6 +64,7 @@ public void Expansion_with_offset_zero_is_valid() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expansion_with_offset_negative_is_invalid() { var expansion = new ExpansionTerm("foo", offset: -1); @@ -68,6 +73,7 @@ public void Expansion_with_offset_negative_is_invalid() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expansion_with_limit_zero_is_invalid() { var expansion = new ExpansionTerm("foo", limit: 0); @@ -76,6 +82,7 @@ public void Expansion_with_limit_zero_is_invalid() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expansion_with_limit_negative_is_invalid() { var expansion = new ExpansionTerm("foo", limit: -1); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs index eeae6f68..e83c253c 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs @@ -30,6 +30,7 @@ public class ExpandExtensionTests private static string resource = "bar"; [TestMethod] + [TestCategory("Impl.Linq")] public void Expand_one_link() { var harness = TestHarness.Create(url, resource); @@ -42,6 +43,7 @@ public void Expand_one_link() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expand_multiple_links() { var harness = TestHarness.Create(url, resource); @@ -55,6 +57,7 @@ public void Expand_multiple_links() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expand_collection_query_with_offset() { var harness = TestHarness.Create(url, resource); @@ -67,6 +70,7 @@ public void Expand_collection_query_with_offset() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expand_collection_query_with_limit() { var harness = TestHarness.Create(url, resource); @@ -79,6 +83,7 @@ public void Expand_collection_query_with_limit() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expand_collection_query_with_both_parameters() { var harness = TestHarness.Create(url, resource); @@ -91,6 +96,7 @@ public void Expand_collection_query_with_both_parameters() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expand_all_the_things() { var harness = TestHarness.Create(url, resource); @@ -105,6 +111,7 @@ public void Expand_all_the_things() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expand_throws_if_used_on_an_attribute() { var harness = TestHarness.Create(url, resource); @@ -116,6 +123,7 @@ public void Expand_throws_if_used_on_an_attribute() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expand_throws_if_parameters_are_supplied_for_link() { var harness = TestHarness.Create(url, resource); @@ -127,6 +135,7 @@ public void Expand_throws_if_parameters_are_supplied_for_link() } [TestMethod] + [TestCategory("Impl.Linq")] public void Expand_throws_if_syntax_is_dumb() { var harness = TestHarness.Create(url, resource); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs index f7370463..d3b4387a 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs @@ -28,6 +28,7 @@ public class FilterExtensionTests private static string resource = "bar"; [TestMethod] + [TestCategory("Impl.Linq")] public void Filter_with_simple_parameter() { // Arrange @@ -43,6 +44,7 @@ public void Filter_with_simple_parameter() } [TestMethod] + [TestCategory("Impl.Linq")] public void Filter_multiple_calls_are_LIFO() { var harness = TestHarness.Create(url, resource); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs index 51c33c9e..8e692242 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs @@ -29,6 +29,7 @@ public class LimitTests private static string resource = "bar"; [TestMethod] + [TestCategory("Impl.Linq")] public void Take_with_constant_becomes_limit() { var harness = TestHarness.Create(url, resource); @@ -41,6 +42,7 @@ public void Take_with_constant_becomes_limit() } [TestMethod] + [TestCategory("Impl.Linq")] public void Take_with_variable_becomes_limit() { var harness = TestHarness.Create(url, resource); @@ -54,6 +56,7 @@ public void Take_with_variable_becomes_limit() } [TestMethod] + [TestCategory("Impl.Linq")] public void Take_with_function_becomes_limit() { var harness = TestHarness.Create(url, resource); @@ -67,6 +70,7 @@ public void Take_with_function_becomes_limit() } [TestMethod] + [TestCategory("Impl.Linq")] public void Take_multiple_calls_are_LIFO() { var harness = TestHarness.Create(url, resource); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs index 194343a8..4a7e69c6 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs @@ -28,6 +28,7 @@ public class OffsetTests private static string resource = "bar"; [TestMethod] + [TestCategory("Impl.Linq")] public void Filter_with_simple_parameter() { // Arrange @@ -43,6 +44,7 @@ public void Filter_with_simple_parameter() } [TestMethod] + [TestCategory("Impl.Linq")] public void Filter_multiple_calls_are_LIFO() { var harness = TestHarness.Create(url, resource); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs index 301fc3e4..75b34395 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs @@ -31,6 +31,7 @@ public class WhereTests private static string resource = "bar"; [TestMethod] + [TestCategory("Impl.Linq")] public void Where_throws_for_constant() { var harness = TestHarness.Create(url, resource); @@ -47,6 +48,7 @@ public void Where_throws_for_constant() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_throws_for_unsupported_comparison_operators() { var harness = TestHarness.Create(url, resource); @@ -58,6 +60,7 @@ public void Where_throws_for_unsupported_comparison_operators() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_throws_for_more_complex_overloads_of_helper_methods() { var harness = TestHarness.Create(url, resource); @@ -74,6 +77,7 @@ public void Where_throws_for_more_complex_overloads_of_helper_methods() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_throws_for_unsupported_helper_methods() { var harness = TestHarness.Create(url, resource); @@ -85,6 +89,7 @@ public void Where_throws_for_unsupported_helper_methods() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_throws_for_binary_or() { var harness = TestHarness.Create(url, resource); @@ -96,6 +101,7 @@ public void Where_throws_for_binary_or() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_attribute_equals() { var harness = TestHarness.Create(url, resource); @@ -108,6 +114,7 @@ public void Where_attribute_equals() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_attribute_equals_using_helper_method() { var harness = TestHarness.Create(url, resource); @@ -120,6 +127,7 @@ public void Where_attribute_equals_using_helper_method() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_attribute_starts_with() { var harness = TestHarness.Create(url, resource); @@ -132,6 +140,7 @@ public void Where_attribute_starts_with() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_attribute_ends_with() { var harness = TestHarness.Create(url, resource); @@ -144,6 +153,7 @@ public void Where_attribute_ends_with() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_attribute_contains() { var harness = TestHarness.Create(url, resource); @@ -156,6 +166,7 @@ public void Where_attribute_contains() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_multiple_attributes_with_and() { var harness = TestHarness.Create(url, resource); @@ -168,6 +179,7 @@ public void Where_multiple_attributes_with_and() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_multiple_wheres() { var harness = TestHarness.Create(url, resource); @@ -181,6 +193,7 @@ public void Where_multiple_wheres() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_date_attribute_greater_than() { var harness = TestHarness.Create(url, resource); @@ -194,6 +207,7 @@ public void Where_date_attribute_greater_than() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_date_attribute_greater_than_or_equalto() { var harness = TestHarness.Create(url, resource); @@ -207,6 +221,7 @@ public void Where_date_attribute_greater_than_or_equalto() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_date_attribute_less_than() { var harness = TestHarness.Create(url, resource); @@ -220,6 +235,7 @@ public void Where_date_attribute_less_than() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_date_attribute_less_than_or_equalto() { var harness = TestHarness.Create(url, resource); @@ -233,6 +249,7 @@ public void Where_date_attribute_less_than_or_equalto() } [TestMethod] + [TestCategory("Impl.Linq")] public void Where_date_attribute_between() { var harness = TestHarness.Create(url, resource); @@ -245,17 +262,5 @@ public void Where_date_attribute_between() harness.WasCalledWithArguments("createdAt=(2015-01-01T00:00:00.000Z,2015-12-31T23:59:59.000Z]"); } - - [TestMethod] - public void Where_date_using_shorthand() - { - var harness = TestHarness.Create(url, resource); - - harness.Queryable - .Where(x => x.CreatedAt == Shorthand.Year(2015)) - .ToList(); - - harness.WasCalledWithArguments("createdAt=2015"); - } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs new file mode 100644 index 00000000..7d0e6b76 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs @@ -0,0 +1,121 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stormpath.SDK.Account; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class WithinExtensionTests + { + private static string url = "http://f.oo"; + private static string resource = "bar"; + + [TestMethod] + [TestCategory("Impl.Linq")] + public void Within_throws_when_using_outside_LINQ() + { + Should.Throw(() => + { + var dto = new DateTimeOffset(DateTime.Now); + var test = dto.Within(2015); + }); + } + + [TestMethod] + [TestCategory("Impl.Linq")] + public void Where_date_using_shorthand_for_year() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.CreatedAt.Within(2015)) + .ToList(); + + harness.WasCalledWithArguments("createdAt=2015"); + } + + [TestMethod] + [TestCategory("Impl.Linq")] + public void Where_date_using_shorthand_for_month() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01)) + .ToList(); + + harness.WasCalledWithArguments("createdAt=2015-01"); + } + + [TestMethod] + [TestCategory("Impl.Linq")] + public void Where_date_using_shorthand_for_day() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01, 01)) + .ToList(); + + harness.WasCalledWithArguments("createdAt=2015-01-01"); + } + + [TestMethod] + [TestCategory("Impl.Linq")] + public void Where_date_using_shorthand_for_hour() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01, 01, 12)) + .ToList(); + + harness.WasCalledWithArguments("createdAt=2015-01-01T12"); + } + + [TestMethod] + [TestCategory("Impl.Linq")] + public void Where_date_using_shorthand_for_minute() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30)) + .ToList(); + + harness.WasCalledWithArguments("createdAt=2015-01-01T12:30"); + } + + [TestMethod] + [TestCategory("Impl.Linq")] + public void Where_date_using_shorthand_for_second() + { + var harness = TestHarness.Create(url, resource); + + harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30, 31)) + .ToList(); + + harness.WasCalledWithArguments("createdAt=2015-01-01T12:30:31"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 25f9c5bb..ea6091b9 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -70,6 +70,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs b/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs index fddddce2..799d8814 100644 --- a/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs +++ b/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs @@ -1,14 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// namespace Stormpath.SDK.Application { public enum ApplicationStatus { + /// + /// Accounts are prevented from logging into this application. + /// Disabled = 0, + + /// + /// Accounts are allowed to log into this application. + /// Enabled = 1 } } diff --git a/Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs b/Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs index ac855606..4f30a636 100644 --- a/Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs @@ -17,7 +17,6 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Stormpath.SDK.Resource; namespace Stormpath.SDK.DataStore { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs index c3001d76..c97295ea 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs @@ -76,6 +76,11 @@ Task ITenantActions.GetAccountsAsync() throw new NotImplementedException(); } + IApplicationList ITenantActions.GetApplications() + { + throw new NotImplementedException(); + } + Task ITenantActions.GetApplicationsAsync() { throw new NotImplementedException(); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs index 605236ea..72d61d57 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs @@ -15,7 +15,6 @@ // limitations under the License. // -using System; using System.Collections.Generic; using System.Linq; using Remotion.Linq; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs index 127d5029..4e08789b 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs @@ -16,6 +16,7 @@ // using System; +using System.Linq; using System.Linq.Expressions; using Remotion.Linq; using Remotion.Linq.Clauses; @@ -40,72 +41,30 @@ public static CollectionResourceRequestModel GenerateRequestModel(QueryModel que public override void VisitResultOperator(ResultOperatorBase resultOperator, QueryModel queryModel, int index) { + if (IsUnsupportedResultOperator(resultOperator)) + throw new NotSupportedException("One or more LINQ operators are not supported."); + // Todo Any // TODO First // TODO Count/LongCount // Todo DefaultIfEmpty // Todo ElementAt[OrDefault] // TODO Single - var takeResultOperator = resultOperator as TakeResultOperator; - if (takeResultOperator != null) - { - var expression = takeResultOperator.Count; - if (expression.NodeType == ExpressionType.Constant) - { - ParsedModel.Limit = (int)((ConstantExpression)expression).Value; - } - else - { - throw new NotSupportedException("Unsupported expression in Take clause."); - } - + if (HandleTakeResultOperator(resultOperator)) return; // done - } - var skipResultOperator = resultOperator as SkipResultOperator; - if (skipResultOperator != null) - { - var expression = skipResultOperator.Count as ConstantExpression; - if (expression == null) - throw new NotSupportedException("Unsupported expression in Skip clause."); + if (HandleSkipResultOperator(resultOperator)) + return; // done - ParsedModel.Offset = (int)expression.Value; + if (HandleExpandExtensionResultOperator(resultOperator)) return; // done - } - var expandResultOperator = resultOperator as ExpandResultOperator; - if (expandResultOperator != null) - { - var methodCallExpression = expandResultOperator.KeySelector as MethodCallExpression; - if (methodCallExpression == null) - throw new NotSupportedException("Expand must be used on a link property method."); - - var expandField = string.Empty; - if (LinkMethodNameTranslator.TryGetValue(methodCallExpression.Method.Name, out expandField)) - { - bool paginationParametersPresent = - expandResultOperator?.Offset?.Value != null || - expandResultOperator?.Limit?.Value != null; - if (paginationParametersPresent) - throw new NotSupportedException("Pagination options cannot be used on link-only properties."); - - ParsedModel.Expansions.Add(new ExpansionTerm(expandField)); - return; // done - } - - if (CollectionLinkMethodTranslator.TryGetValue(methodCallExpression.Method.Name, out expandField)) - { - ParsedModel.Expansions.Add(new ExpansionTerm(expandField, - (int?)expandResultOperator.Offset.Value, - (int?)expandResultOperator.Limit.Value)); - return; // done - } - - throw new NotSupportedException("The selected method does not support expansions."); - } + base.VisitResultOperator(resultOperator, queryModel, index); + } - bool isUnsupported = - resultOperator is AllResultOperator || + private bool IsUnsupportedResultOperator(ResultOperatorBase resultOperator) + { + return resultOperator is AllResultOperator || resultOperator is AggregateResultOperator || resultOperator is AggregateFromSeedResultOperator || resultOperator is AverageResultOperator || @@ -122,30 +81,137 @@ resultOperator is OfTypeResultOperator || resultOperator is ReverseResultOperator || resultOperator is SumResultOperator || resultOperator is UnionResultOperator; - if (isUnsupported) + } + + private bool HandleTakeResultOperator(ResultOperatorBase resultOperator) + { + var takeResultOperator = resultOperator as TakeResultOperator; + if (takeResultOperator == null) + return false; + + var expression = takeResultOperator.Count; + if (expression.NodeType == ExpressionType.Constant) { - throw new NotSupportedException("One or more LINQ operators are not supported."); + ParsedModel.Limit = (int)((ConstantExpression)expression).Value; + } + else + { + throw new NotSupportedException("Unsupported expression in Take clause."); } - base.VisitResultOperator(resultOperator, queryModel, index); + return true; } - public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index) + private bool HandleSkipResultOperator(ResultOperatorBase resultOperator) { - // Handle custom .Filter extension method - var filterClause = whereClause as FilterClause; - if (filterClause != null) + var skipResultOperator = resultOperator as SkipResultOperator; + if (skipResultOperator == null) + return false; + + var expression = skipResultOperator.Count as ConstantExpression; + if (expression == null) + throw new NotSupportedException("Unsupported expression in Skip clause."); + + ParsedModel.Offset = (int)expression.Value; + + return true; + } + + private bool HandleExpandExtensionResultOperator(ResultOperatorBase resultOperator) + { + var expandResultOperator = resultOperator as ExpandResultOperator; + if (expandResultOperator == null) + return false; + + var methodCallExpression = expandResultOperator.KeySelector as MethodCallExpression; + if (methodCallExpression == null) + throw new NotSupportedException("Expand must be used on a link property method."); + + var expandField = string.Empty; + if (LinkMethodNameTranslator.TryGetValue(methodCallExpression.Method.Name, out expandField)) { - ParsedModel.FilterTerm = filterClause.Term; - return; // done + bool paginationParametersPresent = + expandResultOperator?.Offset?.Value != null || + expandResultOperator?.Limit?.Value != null; + if (paginationParametersPresent) + throw new NotSupportedException("Pagination options cannot be used on link-only properties."); + + ParsedModel.Expansions.Add(new ExpansionTerm(expandField)); + return true; // done + } + + if (CollectionLinkMethodNameTranslator.TryGetValue(methodCallExpression.Method.Name, out expandField)) + { + ParsedModel.Expansions.Add(new ExpansionTerm(expandField, + (int?)expandResultOperator.Offset.Value, + (int?)expandResultOperator.Limit.Value)); + return true; // done } + throw new NotSupportedException("The selected method does not support expansions."); + } + + public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index) + { + // Handle simple cases + if (HandleWhereFilterExtensionMethod(whereClause)) + return; // done + if (HandleWhereWithinDateExtensionMethod(whereClause)) + return; // done + this.ParsedModel.AddAttributeTerms( CollectionResourceWhereExpressionVisitor.GenerateModels(whereClause.Predicate)); base.VisitWhereClause(whereClause, queryModel, index); } + private bool HandleWhereFilterExtensionMethod(WhereClause whereClause) + { + var filterClause = whereClause as FilterClause; + if (filterClause == null) + return false; + + ParsedModel.FilterTerm = filterClause.Term; + return true; + } + + private bool HandleWhereWithinDateExtensionMethod(WhereClause whereClause) + { + var methodCall = whereClause.Predicate as MethodCallExpression; + bool isWithinMethodCall = methodCall?.Method.DeclaringType == typeof(CollectionResourceQueryableWithinExtensions); + if (!isWithinMethodCall) + return false; + + var fieldName = string.Empty; + var methodCallMember = methodCall.Arguments[0] as MemberExpression; + bool validField = methodCallMember != null && DatetimeFieldNameTranslator.TryGetValue(methodCallMember.Member.Name, out fieldName); + if (!validField) + throw new NotSupportedException("Within must be used on a supported datetime field."); + + var numberOfConstantArgs = methodCall.Arguments.Count - 1; + var yearArg = (int)(methodCall.Arguments[1] as ConstantExpression).Value; + int? monthArg = null, + dayArg = null, + hourArg = null, + minuteArg = null, + secondArg = null; + + if (methodCall.Arguments.Count >= 3) + monthArg = (methodCall.Arguments?[2] as ConstantExpression)?.Value as int?; + if (methodCall.Arguments.Count >= 4) + dayArg = (methodCall.Arguments?[3] as ConstantExpression)?.Value as int?; + if (methodCall.Arguments.Count >= 5) + hourArg = (methodCall.Arguments?[4] as ConstantExpression)?.Value as int?; + if (methodCall.Arguments.Count >= 6) + minuteArg = (methodCall.Arguments?[5] as ConstantExpression)?.Value as int?; + if (methodCall.Arguments.Count == 7) + secondArg = (methodCall.Arguments?[6] as ConstantExpression)?.Value as int?; + + this.ParsedModel.AddAttributeTerm(new DatetimeShorthandAttributeTermModel(fieldName, yearArg, monthArg, dayArg, hourArg, minuteArg, secondArg)); + + return true; + } + public override void VisitOrdering(Ordering ordering, QueryModel queryModel, OrderByClause orderByClause, int index) { var memberAccessor = ordering.Expression as MemberExpression; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceRequestModelCompiler.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceRequestModelCompiler.cs index e84de0b8..7b036f5e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceRequestModelCompiler.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceRequestModelCompiler.cs @@ -35,9 +35,9 @@ public static List GetArguments(CollectionResourceRequestModel model) arguments.Add("q", model.FilterTerm); // From .Where(x => ?) - if (model.AttributeTerms.Count > 0) + if (model.StringAttributeTerms.Count > 0) { - foreach (var term in model.AttributeTerms) + foreach (var term in model.StringAttributeTerms) { switch (term.MatchType) { @@ -93,6 +93,53 @@ public static List GetArguments(CollectionResourceRequestModel model) } } + // From .Where(x => [createdAt|modifiedAt].Within(???) + if (model.DatetimeShorthandAttributeTerms.Count > 0) + { + foreach (var term in model.DatetimeShorthandAttributeTerms) + { + var shorthandAttribute = new StringBuilder(); + + shorthandAttribute.Append(term.Year); + if (term.Month.HasValue) + { + shorthandAttribute.Append($"-{term.Month.Value:D2}"); + } + + if (term.Month.HasValue && + term.Day.HasValue) + { + shorthandAttribute.Append($"-{term.Day.Value:D2}"); + } + + if (term.Month.HasValue && + term.Day.HasValue && + term.Hour.HasValue) + { + shorthandAttribute.Append($"T{term.Hour.Value:D2}"); + } + + if (term.Month.HasValue && + term.Day.HasValue && + term.Hour.HasValue && + term.Minute.HasValue) + { + shorthandAttribute.Append($":{term.Minute.Value:D2}"); + } + + if (term.Month.HasValue && + term.Day.HasValue && + term.Hour.HasValue && + term.Hour.HasValue && + term.Second.HasValue) + { + shorthandAttribute.Append($":{term.Second.Value:D2}"); + } + + arguments.Add(term.Field, shorthandAttribute.ToString()); + } + } + // From .Take() if (model?.Limit > 0) arguments.Add("limit", model.Limit.Value.ToString()); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/CollectionResourceRequestModel.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/CollectionResourceRequestModel.cs index 4d929bb6..cb67d57d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/CollectionResourceRequestModel.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/CollectionResourceRequestModel.cs @@ -28,10 +28,12 @@ internal class CollectionResourceRequestModel public string FilterTerm { get; set; } - public List AttributeTerms { get; } = new List(); + public List StringAttributeTerms { get; } = new List(); public List DatetimeAttributeTerms { get; } = new List(); + public List DatetimeShorthandAttributeTerms { get; } = new List(); + public List OrderByTerms { get; } = new List(); public List Expansions { get; } = new List(); @@ -41,7 +43,7 @@ public void AddAttributeTerm(AbstractAttributeTermModel model) var modelAsString = model as StringAttributeTermModel; if (modelAsString != null) { - this.AttributeTerms.Add(modelAsString); + this.StringAttributeTerms.Add(modelAsString); return; // done } @@ -52,6 +54,13 @@ public void AddAttributeTerm(AbstractAttributeTermModel model) return; // done } + var modelAsDatetimeShorthand = model as DatetimeShorthandAttributeTermModel; + if (modelAsDatetimeShorthand != null) + { + this.DatetimeShorthandAttributeTerms.Add(modelAsDatetimeShorthand); + return; // done + } + throw new NotSupportedException("Unknown attribute term model type."); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeShorthandAttributeTermModel.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeShorthandAttributeTermModel.cs new file mode 100644 index 00000000..ef510836 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeShorthandAttributeTermModel.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Stormpath.SDK.Impl.Linq.RequestModel +{ + internal class DatetimeShorthandAttributeTermModel : AbstractAttributeTermModel + { + public DatetimeShorthandAttributeTermModel( + string field, + int year, + int? month = null, + int? day = null, + int? hour = null, + int? minute = null, + int? second = null) + { + this.Field = field; + + this.Year = year; + this.Month = month; + this.Day = day; + this.Hour = hour; + this.Minute = minute; + this.Second = second; + } + + public int Year { get; private set; } + + public int? Month { get; private set; } + + public int? Day { get; private set; } + + public int? Hour { get; private set; } + + public int? Minute { get; private set; } + + public int? Second { get; private set; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/CollectionLinkMethodNameTranslator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/CollectionLinkMethodNameTranslator.cs index b99a2390..fa643265 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/CollectionLinkMethodNameTranslator.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/StaticNameTranslators/CollectionLinkMethodNameTranslator.cs @@ -19,7 +19,7 @@ namespace Stormpath.SDK.Impl.Linq.StaticNameTranslators { - internal static class CollectionLinkMethodTranslator + internal static class CollectionLinkMethodNameTranslator { private static readonly Dictionary ValidNames = new Dictionary() { diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs b/Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs index 0c691b86..8d65a379 100644 --- a/Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs +++ b/Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs @@ -36,7 +36,7 @@ public static ICollectionResourceQueryable Filter(this ICollectionResource } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single class", Justification = "")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single class", Justification = "Extension methods grouped together")] public static class CollectionResourceQueryableExpandExtensions { public static ICollectionResourceQueryable Expand(this ICollectionResourceQueryable source, Expression> keySelector) @@ -61,4 +61,38 @@ public static ICollectionResourceQueryable Expand(this ICollectionRe Expression.Constant(limit, typeof(int?)))); } } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single class", Justification = "Extension methods grouped together")] + public static class CollectionResourceQueryableWithinExtensions + { + public static bool Within(this DateTimeOffset field, int year) + { + throw new NotSupportedException("Direct calls to Within() are not supported. Use from inside a LINQ Where predicate."); + } + + public static bool Within(this DateTimeOffset field, int year, int month) + { + throw new NotSupportedException("Direct calls to Within() are not supported. Use from inside a LINQ Where predicate."); + } + + public static bool Within(this DateTimeOffset field, int year, int month, int day) + { + throw new NotSupportedException("Direct calls to Within() are not supported. Use from inside a LINQ Where predicate."); + } + + public static bool Within(this DateTimeOffset field, int year, int month, int day, int hour) + { + throw new NotSupportedException("Direct calls to Within() are not supported. Use from inside a LINQ Where predicate."); + } + + public static bool Within(this DateTimeOffset field, int year, int month, int day, int hour, int minute) + { + throw new NotSupportedException("Direct calls to Within() are not supported. Use from inside a LINQ Where predicate."); + } + + public static bool Within(this DateTimeOffset field, int year, int month, int day, int hour, int minute, int second) + { + throw new NotSupportedException("Direct calls to Within() are not supported. Use from inside a LINQ Where predicate."); + } + } } diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index be05fcaf..e0d99568 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -90,6 +90,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs index 61f8da3a..9d32b121 100644 --- a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs +++ b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenant.cs @@ -15,7 +15,6 @@ // limitations under the License. // -using System.Threading.Tasks; using Stormpath.SDK.Resource; namespace Stormpath.SDK.Tenant From 07c33f121dea2f9eb8d9172b1b953fcb48ac0802 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 10 Aug 2015 15:10:59 -0700 Subject: [PATCH 028/238] Test cleanup --- .../Impl/Linq/OrderByTests.cs | 4 +++ .../Impl/Linq/UnsupportedFilterTests.cs | 25 +++++++++++++++++++ .../Impl/Utility/Iso8601Tests.cs | 2 ++ 3 files changed, 31 insertions(+) diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs index 8f582870..2404444c 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs @@ -29,6 +29,7 @@ public class OrderByTests private static string resource = "bar"; [TestMethod] + [TestCategory("Impl.Linq")] public void Skip_becomes_offset() { var harness = TestHarness.Create(url, resource); @@ -41,6 +42,7 @@ public void Skip_becomes_offset() } [TestMethod] + [TestCategory("Impl.Linq")] public void Skip_with_variable_becomes_offset() { var harness = TestHarness.Create(url, resource); @@ -54,6 +56,7 @@ public void Skip_with_variable_becomes_offset() } [TestMethod] + [TestCategory("Impl.Linq")] public void Skip_with_function_becomes_offset() { var harness = TestHarness.Create(url, resource); @@ -67,6 +70,7 @@ public void Skip_with_function_becomes_offset() } [TestMethod] + [TestCategory("Impl.Linq")] public void Skip_multiple_calls_are_LIFO() { var harness = TestHarness.Create(url, resource); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs index ca027eed..5adf6bca 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs @@ -31,6 +31,7 @@ public class UnsupportedFilterTests private static string resource = "bar"; [TestMethod] + [TestCategory("Impl.Linq")] public void Aggregate_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -42,6 +43,7 @@ public void Aggregate_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void All_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -53,6 +55,7 @@ public void All_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Average_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -64,6 +67,7 @@ public void Average_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Cast_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -75,6 +79,7 @@ public void Cast_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Concat_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -86,6 +91,7 @@ public void Concat_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Contains_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -97,6 +103,7 @@ public void Contains_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Distinct_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -108,6 +115,7 @@ public void Distinct_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Except_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -119,6 +127,7 @@ public void Except_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void GroupBy_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -130,6 +139,7 @@ public void GroupBy_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void GroupJoin_clause_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -145,6 +155,7 @@ public void GroupJoin_clause_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Intersect_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -156,6 +167,7 @@ public void Intersect_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Join_clause_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -170,6 +182,7 @@ public void Join_clause_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Last_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -186,6 +199,7 @@ public void Last_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Max_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -197,6 +211,7 @@ public void Max_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Min_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -208,6 +223,7 @@ public void Min_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void OfType_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -219,6 +235,7 @@ public void OfType_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Reverse_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -230,6 +247,7 @@ public void Reverse_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Select_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -243,6 +261,7 @@ public void Select_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void SelectMany_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -254,6 +273,7 @@ public void SelectMany_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void SequenceEqual_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -265,6 +285,7 @@ public void SequenceEqual_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void SkipWhile_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -276,6 +297,7 @@ public void SkipWhile_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Sum_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -287,6 +309,7 @@ public void Sum_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void TakeWhile_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -298,6 +321,7 @@ public void TakeWhile_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Union_is_unsupported() { var harness = TestHarness.Create(url, resource); @@ -309,6 +333,7 @@ public void Union_is_unsupported() } [TestMethod] + [TestCategory("Impl.Linq")] public void Zip_is_unsupported() { var harness = TestHarness.Create(url, resource); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601Tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601Tests.cs index 0f6dbaea..8fecd670 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601Tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601Tests.cs @@ -26,6 +26,7 @@ namespace Stormpath.SDK.Tests.Impl.Utility public class Iso8601Tests { [TestMethod] + [TestCategory("Impl.Utility")] public void Format_from_local_date() { // Midnight Pacific Time, Jan 1, 2015 = 2015-01-01 08:00 UTC @@ -40,6 +41,7 @@ public void Format_from_local_date() } [TestMethod] + [TestCategory("Impl.Utility")] public void Format_from_UTC_date() { var alreadyInUtc = new DateTimeOffset(2016, 02, 01, 12, 00, 00, TimeSpan.Zero); From 9c6bd961af461668ceaf0ee88afd45be55b647d9 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 10 Aug 2015 15:38:06 -0700 Subject: [PATCH 029/238] Update test harness name --- .../Impl/Linq/ExpandExtensionTests.cs | 18 +++---- .../Impl/Linq/FilterExtensionTests.cs | 4 +- .../Impl/Linq/LimitTests.cs | 8 +-- .../{TestHarness.cs => LinqTestHarness.cs} | 6 +-- .../Impl/Linq/OffsetTests.cs | 4 +- .../Impl/Linq/OrderByTests.cs | 8 +-- .../Impl/Linq/UnsupportedFilterTests.cs | 50 +++++++++---------- .../Impl/Linq/WhereTests.cs | 34 ++++++------- .../Impl/Linq/WithinExtensionTests.cs | 12 ++--- .../Stormpath.SDK.Tests.csproj | 2 +- 10 files changed, 73 insertions(+), 73 deletions(-) rename Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/{TestHarness.cs => LinqTestHarness.cs} (90%) diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs index e83c253c..2ba1e939 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs @@ -33,7 +33,7 @@ public class ExpandExtensionTests [TestCategory("Impl.Linq")] public void Expand_one_link() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetDirectoryAsync()) @@ -46,7 +46,7 @@ public void Expand_one_link() [TestCategory("Impl.Linq")] public void Expand_multiple_links() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetDirectoryAsync()) @@ -60,7 +60,7 @@ public void Expand_multiple_links() [TestCategory("Impl.Linq")] public void Expand_collection_query_with_offset() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetGroupsAsync(), offset: 10) @@ -73,7 +73,7 @@ public void Expand_collection_query_with_offset() [TestCategory("Impl.Linq")] public void Expand_collection_query_with_limit() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetGroupsAsync(), limit: 20) @@ -86,7 +86,7 @@ public void Expand_collection_query_with_limit() [TestCategory("Impl.Linq")] public void Expand_collection_query_with_both_parameters() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetGroupsAsync(), 5, 15) @@ -99,7 +99,7 @@ public void Expand_collection_query_with_both_parameters() [TestCategory("Impl.Linq")] public void Expand_all_the_things() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Expand(x => x.GetTenantAsync()) @@ -114,7 +114,7 @@ public void Expand_all_the_things() [TestCategory("Impl.Linq")] public void Expand_throws_if_used_on_an_attribute() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -126,7 +126,7 @@ public void Expand_throws_if_used_on_an_attribute() [TestCategory("Impl.Linq")] public void Expand_throws_if_parameters_are_supplied_for_link() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -138,7 +138,7 @@ public void Expand_throws_if_parameters_are_supplied_for_link() [TestCategory("Impl.Linq")] public void Expand_throws_if_syntax_is_dumb() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs index d3b4387a..b1f611a2 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs @@ -32,7 +32,7 @@ public class FilterExtensionTests public void Filter_with_simple_parameter() { // Arrange - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); // Act harness.Queryable @@ -47,7 +47,7 @@ public void Filter_with_simple_parameter() [TestCategory("Impl.Linq")] public void Filter_multiple_calls_are_LIFO() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Filter("Joe") diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs index 8e692242..06627186 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs @@ -32,7 +32,7 @@ public class LimitTests [TestCategory("Impl.Linq")] public void Take_with_constant_becomes_limit() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Take(10) @@ -45,7 +45,7 @@ public void Take_with_constant_becomes_limit() [TestCategory("Impl.Linq")] public void Take_with_variable_becomes_limit() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); var limit = 20; harness.Queryable @@ -59,7 +59,7 @@ public void Take_with_variable_becomes_limit() [TestCategory("Impl.Linq")] public void Take_with_function_becomes_limit() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); var limitFunc = new Func(() => 25); harness.Queryable @@ -73,7 +73,7 @@ public void Take_with_function_becomes_limit() [TestCategory("Impl.Linq")] public void Take_multiple_calls_are_LIFO() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Take(10).Take(5) diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs similarity index 90% rename from Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs rename to Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs index a3f24187..e2bda8ed 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/TestHarness.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs @@ -22,7 +22,7 @@ namespace Stormpath.SDK.Tests.Impl.Linq { - public class TestHarness + public class LinqTestHarness where T : IResource { public IDataStore DataStore { get; private set; } @@ -38,12 +38,12 @@ public void WasCalledWithArguments(string arguments) DataStore.Received().GetCollection($"{Url}/{Resource}?{arguments}"); } - public static TestHarness Create(string url, string resource) + public static LinqTestHarness Create(string url, string resource) where TType : IResource { var ds = Substitute.For(); - return new TestHarness() + return new LinqTestHarness() { DataStore = ds, Resource = resource, diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs index 4a7e69c6..ce76e810 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs @@ -32,7 +32,7 @@ public class OffsetTests public void Filter_with_simple_parameter() { // Arrange - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); // Act harness.Queryable @@ -47,7 +47,7 @@ public void Filter_with_simple_parameter() [TestCategory("Impl.Linq")] public void Filter_multiple_calls_are_LIFO() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Filter("Joe") diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs index 2404444c..8e49ec9b 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs @@ -32,7 +32,7 @@ public class OrderByTests [TestCategory("Impl.Linq")] public void Skip_becomes_offset() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Skip(10) @@ -45,7 +45,7 @@ public void Skip_becomes_offset() [TestCategory("Impl.Linq")] public void Skip_with_variable_becomes_offset() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); var offset = 20; harness.Queryable @@ -59,7 +59,7 @@ public void Skip_with_variable_becomes_offset() [TestCategory("Impl.Linq")] public void Skip_with_function_becomes_offset() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); var offsetFunc = new Func(() => 25); harness.Queryable @@ -73,7 +73,7 @@ public void Skip_with_function_becomes_offset() [TestCategory("Impl.Linq")] public void Skip_multiple_calls_are_LIFO() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Skip(10).Skip(5) diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs index 5adf6bca..f26c8c48 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs @@ -34,7 +34,7 @@ public class UnsupportedFilterTests [TestCategory("Impl.Linq")] public void Aggregate_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -46,7 +46,7 @@ public void Aggregate_is_unsupported() [TestCategory("Impl.Linq")] public void All_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -58,7 +58,7 @@ public void All_is_unsupported() [TestCategory("Impl.Linq")] public void Average_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -70,7 +70,7 @@ public void Average_is_unsupported() [TestCategory("Impl.Linq")] public void Cast_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -82,7 +82,7 @@ public void Cast_is_unsupported() [TestCategory("Impl.Linq")] public void Concat_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -94,7 +94,7 @@ public void Concat_is_unsupported() [TestCategory("Impl.Linq")] public void Contains_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -106,7 +106,7 @@ public void Contains_is_unsupported() [TestCategory("Impl.Linq")] public void Distinct_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -118,7 +118,7 @@ public void Distinct_is_unsupported() [TestCategory("Impl.Linq")] public void Except_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -130,7 +130,7 @@ public void Except_is_unsupported() [TestCategory("Impl.Linq")] public void GroupBy_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -142,7 +142,7 @@ public void GroupBy_is_unsupported() [TestCategory("Impl.Linq")] public void GroupJoin_clause_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -158,7 +158,7 @@ public void GroupJoin_clause_is_unsupported() [TestCategory("Impl.Linq")] public void Intersect_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -170,7 +170,7 @@ public void Intersect_is_unsupported() [TestCategory("Impl.Linq")] public void Join_clause_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -185,7 +185,7 @@ public void Join_clause_is_unsupported() [TestCategory("Impl.Linq")] public void Last_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -202,7 +202,7 @@ public void Last_is_unsupported() [TestCategory("Impl.Linq")] public void Max_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -214,7 +214,7 @@ public void Max_is_unsupported() [TestCategory("Impl.Linq")] public void Min_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -226,7 +226,7 @@ public void Min_is_unsupported() [TestCategory("Impl.Linq")] public void OfType_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -238,7 +238,7 @@ public void OfType_is_unsupported() [TestCategory("Impl.Linq")] public void Reverse_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -250,7 +250,7 @@ public void Reverse_is_unsupported() [TestCategory("Impl.Linq")] public void Select_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -264,7 +264,7 @@ public void Select_is_unsupported() [TestCategory("Impl.Linq")] public void SelectMany_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -276,7 +276,7 @@ public void SelectMany_is_unsupported() [TestCategory("Impl.Linq")] public void SequenceEqual_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -288,7 +288,7 @@ public void SequenceEqual_is_unsupported() [TestCategory("Impl.Linq")] public void SkipWhile_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -300,7 +300,7 @@ public void SkipWhile_is_unsupported() [TestCategory("Impl.Linq")] public void Sum_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -312,7 +312,7 @@ public void Sum_is_unsupported() [TestCategory("Impl.Linq")] public void TakeWhile_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -324,7 +324,7 @@ public void TakeWhile_is_unsupported() [TestCategory("Impl.Linq")] public void Union_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -336,7 +336,7 @@ public void Union_is_unsupported() [TestCategory("Impl.Linq")] public void Zip_is_unsupported() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs index 75b34395..f121b80a 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs @@ -34,7 +34,7 @@ public class WhereTests [TestCategory("Impl.Linq")] public void Where_throws_for_constant() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -51,7 +51,7 @@ public void Where_throws_for_constant() [TestCategory("Impl.Linq")] public void Where_throws_for_unsupported_comparison_operators() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -63,7 +63,7 @@ public void Where_throws_for_unsupported_comparison_operators() [TestCategory("Impl.Linq")] public void Where_throws_for_more_complex_overloads_of_helper_methods() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -80,7 +80,7 @@ public void Where_throws_for_more_complex_overloads_of_helper_methods() [TestCategory("Impl.Linq")] public void Where_throws_for_unsupported_helper_methods() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -92,7 +92,7 @@ public void Where_throws_for_unsupported_helper_methods() [TestCategory("Impl.Linq")] public void Where_throws_for_binary_or() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); Should.Throw(() => { @@ -104,7 +104,7 @@ public void Where_throws_for_binary_or() [TestCategory("Impl.Linq")] public void Where_attribute_equals() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.Email == "tk421@deathstar.co") @@ -117,7 +117,7 @@ public void Where_attribute_equals() [TestCategory("Impl.Linq")] public void Where_attribute_equals_using_helper_method() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.Email.Equals("tk421@deathstar.co")) @@ -130,7 +130,7 @@ public void Where_attribute_equals_using_helper_method() [TestCategory("Impl.Linq")] public void Where_attribute_starts_with() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.Email.StartsWith("tk421")) @@ -143,7 +143,7 @@ public void Where_attribute_starts_with() [TestCategory("Impl.Linq")] public void Where_attribute_ends_with() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.Email.EndsWith("deathstar.co")) @@ -156,7 +156,7 @@ public void Where_attribute_ends_with() [TestCategory("Impl.Linq")] public void Where_attribute_contains() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.Email.Contains("421")) @@ -169,7 +169,7 @@ public void Where_attribute_contains() [TestCategory("Impl.Linq")] public void Where_multiple_attributes_with_and() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.Email == "tk421@deathstar.co" && x.Username == "tk421") @@ -182,7 +182,7 @@ public void Where_multiple_attributes_with_and() [TestCategory("Impl.Linq")] public void Where_multiple_wheres() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.Email == "tk421@deathstar.co") @@ -196,7 +196,7 @@ public void Where_multiple_wheres() [TestCategory("Impl.Linq")] public void Where_date_attribute_greater_than() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); harness.Queryable @@ -210,7 +210,7 @@ public void Where_date_attribute_greater_than() [TestCategory("Impl.Linq")] public void Where_date_attribute_greater_than_or_equalto() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); harness.Queryable @@ -224,7 +224,7 @@ public void Where_date_attribute_greater_than_or_equalto() [TestCategory("Impl.Linq")] public void Where_date_attribute_less_than() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); harness.Queryable @@ -238,7 +238,7 @@ public void Where_date_attribute_less_than() [TestCategory("Impl.Linq")] public void Where_date_attribute_less_than_or_equalto() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); harness.Queryable @@ -252,7 +252,7 @@ public void Where_date_attribute_less_than_or_equalto() [TestCategory("Impl.Linq")] public void Where_date_attribute_between() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); var testStartDate = new DateTimeOffset(2015, 01, 01, 00, 00, 00, TimeSpan.Zero); var testEndDate = new DateTimeOffset(2015, 12, 31, 23, 59, 59, TimeSpan.Zero); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs index 7d0e6b76..24abb1a8 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs @@ -44,7 +44,7 @@ public void Within_throws_when_using_outside_LINQ() [TestCategory("Impl.Linq")] public void Where_date_using_shorthand_for_year() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.CreatedAt.Within(2015)) @@ -57,7 +57,7 @@ public void Where_date_using_shorthand_for_year() [TestCategory("Impl.Linq")] public void Where_date_using_shorthand_for_month() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01)) @@ -70,7 +70,7 @@ public void Where_date_using_shorthand_for_month() [TestCategory("Impl.Linq")] public void Where_date_using_shorthand_for_day() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01, 01)) @@ -83,7 +83,7 @@ public void Where_date_using_shorthand_for_day() [TestCategory("Impl.Linq")] public void Where_date_using_shorthand_for_hour() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01, 01, 12)) @@ -96,7 +96,7 @@ public void Where_date_using_shorthand_for_hour() [TestCategory("Impl.Linq")] public void Where_date_using_shorthand_for_minute() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30)) @@ -109,7 +109,7 @@ public void Where_date_using_shorthand_for_minute() [TestCategory("Impl.Linq")] public void Where_date_using_shorthand_for_second() { - var harness = TestHarness.Create(url, resource); + var harness = LinqTestHarness.Create(url, resource); harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30, 31)) diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index ea6091b9..a4ae99f2 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -78,7 +78,7 @@ - + From cb6e724379011db7fce2537ebfe1007d9a7ab507 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 10 Aug 2015 17:26:54 -0700 Subject: [PATCH 030/238] First attempt at async queryables --- .../Impl/Linq/AsyncTests.cs | 43 +++++ .../Impl/Linq/LinqTestHarness.cs | 2 +- .../Impl/Linq/WhereTests.cs | 1 - .../Stormpath.SDK.Tests.csproj | 1 + ...cationList.cs => IApplicationAsyncList.cs} | 4 +- .../Impl/Client/DefaultClient.cs | 5 +- .../Impl/Linq/AsyncQueryProviderWrapper.cs | 49 +++++ .../Impl/Linq/AsyncQueryableBase.cs | 41 ++++ ...> CollectionResourceQueryAsyncExecutor.cs} | 45 ++++- .../CollectionResourceQueryModelVisitor.cs | 1 - .../Impl/Linq/IAsyncQueryExecutor.cs | 32 ++++ .../Impl/Linq/IAsyncQueryProvider.cs | 53 ++++++ .../DatetimeShorthandAttributeTermModel.cs | 2 - .../Resource/CollectionResourceQueryable.cs | 8 +- .../Linq/AsyncQueryableExtensions.cs | 165 +++++++++++++++++ .../CollectionResourceQueryableExtensions.cs | 2 +- .../Stormpath.SDK/Linq/IAsyncQueryable.cs | 27 +++ .../Resource/ICollectionResourceQueryable.cs | 4 +- .../Stormpath.SDK/Resource/Shorthand.cs | 175 ------------------ .../Stormpath.SDK/Stormpath.SDK.csproj | 13 +- .../Stormpath.SDK/Tenant/ITenantActions.cs | 4 +- 21 files changed, 475 insertions(+), 202 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/AsyncTests.cs rename Stormpath.SDK/Stormpath.SDK/Application/{IApplicationList.cs => IApplicationAsyncList.cs} (81%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryProviderWrapper.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryableBase.cs rename Stormpath.SDK/Stormpath.SDK/Impl/Linq/{CollectionResourceQueryExecutor.cs => CollectionResourceQueryAsyncExecutor.cs} (54%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/IAsyncQueryExecutor.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Linq/IAsyncQueryProvider.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Linq/AsyncQueryableExtensions.cs rename Stormpath.SDK/Stormpath.SDK/{Resource => Linq}/CollectionResourceQueryableExtensions.cs (99%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Linq/IAsyncQueryable.cs delete mode 100644 Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/AsyncTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/AsyncTests.cs new file mode 100644 index 00000000..64339ff1 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/AsyncTests.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stormpath.SDK.Application; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + [TestClass] + public class MyTestClass + { + private static string url = "http://f.oo"; + private static string resource = "bar"; + + [TestMethod] + [TestCategory("Impl.Linq")] + public async Task FirstAsync() + { + var harness = LinqTestHarness.Create(url, resource); + var applications = harness.Queryable; + + var first = await applications.FirstAsync(); + + first.ShouldNotBe(null); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs index e2bda8ed..41fd6f8e 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs index f121b80a..c86a09be 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs @@ -20,7 +20,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; using Stormpath.SDK.Account; -using Stormpath.SDK.Resource; namespace Stormpath.SDK.Tests.Impl.Linq { diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index a4ae99f2..052411b5 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -69,6 +69,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Application/IApplicationList.cs b/Stormpath.SDK/Stormpath.SDK/Application/IApplicationAsyncList.cs similarity index 81% rename from Stormpath.SDK/Stormpath.SDK/Application/IApplicationList.cs rename to Stormpath.SDK/Stormpath.SDK/Application/IApplicationAsyncList.cs index cebb3f36..0e7b0a85 100644 --- a/Stormpath.SDK/Stormpath.SDK/Application/IApplicationList.cs +++ b/Stormpath.SDK/Stormpath.SDK/Application/IApplicationAsyncList.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -19,7 +19,7 @@ namespace Stormpath.SDK.Application { - public interface IApplicationList : ICollectionResourceQueryable + public interface IApplicationAsyncList : ICollectionResourceQueryable { } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs index c97295ea..72268931 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs @@ -76,12 +76,13 @@ Task ITenantActions.GetAccountsAsync() throw new NotImplementedException(); } - IApplicationList ITenantActions.GetApplications() + IApplicationAsyncList ITenantActions.GetApplications() { + // return new CollectionResourceQueryable() throw new NotImplementedException(); } - Task ITenantActions.GetApplicationsAsync() + Task ITenantActions.GetApplicationsAsync() { throw new NotImplementedException(); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryProviderWrapper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryProviderWrapper.cs new file mode 100644 index 00000000..e38abf5c --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryProviderWrapper.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Remotion.Linq.Parsing.Structure; + +namespace Stormpath.SDK.Impl.Linq +{ + internal class AsyncQueryProviderWrapper : IAsyncQueryProvider + { + private readonly IQueryParser queryParser; + private readonly IAsyncQueryExecutor executor; + + public AsyncQueryProviderWrapper(IQueryParser queryParser, IAsyncQueryExecutor executor) + { + this.queryParser = queryParser; + this.executor = executor; + } + + public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken) + { + var queryModel = queryParser.GetParsedQuery(expression); + + throw new NotImplementedException(); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryableBase.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryableBase.cs new file mode 100644 index 00000000..3d28e3c2 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryableBase.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; +using System.Linq.Expressions; +using Remotion.Linq; +using Remotion.Linq.Parsing.Structure; +using Stormpath.SDK.Linq; + +namespace Stormpath.SDK.Impl.Linq +{ + internal class AsyncQueryableBase : QueryableBase + { + public AsyncQueryableBase(IQueryProvider provider, Expression expression) + : base(provider, expression) + { + } + + public AsyncQueryableBase(IQueryParser queryParser, IAsyncQueryExecutor asyncExecutor) + : base(queryParser, asyncExecutor) + { + this.AsyncProvider = new AsyncQueryProviderWrapper(queryParser, asyncExecutor); + } + + public IAsyncQueryProvider AsyncProvider { get; private set; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryAsyncExecutor.cs similarity index 54% rename from Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryAsyncExecutor.cs index 72d61d57..ffb5d7ed 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryAsyncExecutor.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -15,20 +15,22 @@ // limitations under the License. // +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Remotion.Linq; using Stormpath.SDK.DataStore; namespace Stormpath.SDK.Impl.Linq { - internal class CollectionResourceQueryExecutor : IQueryExecutor + internal class CollectionResourceQueryAsyncExecutor : IAsyncQueryExecutor { private readonly string url; private readonly string resource; private readonly IDataStore dataStore; - public CollectionResourceQueryExecutor(string url, string resource, IDataStore dataStore) + public CollectionResourceQueryAsyncExecutor(string url, string resource, IDataStore dataStore) { this.url = url; this.resource = resource; @@ -36,12 +38,26 @@ public CollectionResourceQueryExecutor(string url, string resource, IDataStore d } public IEnumerable ExecuteCollection(QueryModel queryModel) + { + var arguments = QueryModelToArguments(queryModel); + + return dataStore.GetCollection($"{url}/{resource}?{arguments}"); + } + + public Task ExecuteAsync(QueryModel queryModel) + { + var arguments = QueryModelToArguments(queryModel); + + // return dataStore.GetCollectionAsync($"{url}/{resource}?{arguments}"); + throw new NotImplementedException(); + } + + private static string QueryModelToArguments(QueryModel queryModel) { var model = CollectionResourceQueryModelVisitor.GenerateRequestModel(queryModel); var arguments = CollectionResourceRequestModelCompiler.GetArguments(model); var argumentString = string.Join("&", arguments); - - return dataStore.GetCollection($"{url}/{resource}?{argumentString}"); + return argumentString; } public T ExecuteScalar(QueryModel queryModel) @@ -55,5 +71,24 @@ public T ExecuteSingle(QueryModel queryModel, bool returnDefaultWhenEmpty) ? ExecuteCollection(queryModel).SingleOrDefault() : ExecuteCollection(queryModel).Single(); } + + public Task> ExecuteCollectionAsync(QueryModel queryModel) + { + var model = CollectionResourceQueryModelVisitor.GenerateRequestModel(queryModel); + var arguments = CollectionResourceRequestModelCompiler.GetArguments(model); + var argumentString = string.Join("&", arguments); + + return dataStore.GetCollectionAsync($"{url}/{resource}?{argumentString}"); + } + + public T ExecuteScalarAsync(QueryModel queryModel) + { + throw new NotImplementedException(); + } + + public T ExecuteSingleAsync(QueryModel queryModel, bool returnDefaultWhenEmpty) + { + throw new NotImplementedException(); + } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs index 4e08789b..22dc296d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs @@ -16,7 +16,6 @@ // using System; -using System.Linq; using System.Linq.Expressions; using Remotion.Linq; using Remotion.Linq.Clauses; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/IAsyncQueryExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/IAsyncQueryExecutor.cs new file mode 100644 index 00000000..d32b0484 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/IAsyncQueryExecutor.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Threading.Tasks; +using Remotion.Linq; + +namespace Stormpath.SDK.Impl.Linq +{ + internal interface IAsyncQueryExecutor : IQueryExecutor + { + Task> ExecuteCollectionAsync(QueryModel queryModel); + + T ExecuteScalarAsync(QueryModel queryModel); + + T ExecuteSingleAsync(QueryModel queryModel, bool returnDefaultWhenEmpty); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/IAsyncQueryProvider.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/IAsyncQueryProvider.cs new file mode 100644 index 00000000..436f0d0f --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/IAsyncQueryProvider.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Linq +{ + public interface IAsyncQueryProvider + { + /// + /// Asynchronously executes the query represented by a specified expression tree. + /// + /// An expression tree that represents a LINQ query. + /// + /// A to observe while waiting for the task to complete. + /// + /// + /// A task that represents the asynchronous operation. + /// The task result contains the value that results from executing the specified query. + /// + Task ExecuteAsync(Expression expression, CancellationToken cancellationToken); + + /// + /// Asynchronously executes the strongly-typed query represented by a specified expression tree. + /// + /// The type of the value that results from executing the query. + /// An expression tree that represents a LINQ query. + /// + /// A to observe while waiting for the task to complete. + /// + /// + /// A task that represents the asynchronous operation. + /// The task result contains the value that results from executing the specified query. + /// + Task ExecuteAsync(Expression expression, CancellationToken cancellationToken); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeShorthandAttributeTermModel.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeShorthandAttributeTermModel.cs index ef510836..00015ac0 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeShorthandAttributeTermModel.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/RequestModel/DatetimeShorthandAttributeTermModel.cs @@ -15,8 +15,6 @@ // limitations under the License. // -using System; - namespace Stormpath.SDK.Impl.Linq.RequestModel { internal class DatetimeShorthandAttributeTermModel : AbstractAttributeTermModel diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs index 06ecc2d2..c4d37808 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs @@ -26,7 +26,7 @@ namespace Stormpath.SDK.Impl.Resource { - internal class CollectionResourceQueryable : QueryableBase, ICollectionResourceQueryable + internal class CollectionResourceQueryable : AsyncQueryableBase, ICollectionResourceQueryable { public CollectionResourceQueryable(string url, string resource, IDataStore dataStore) : base(ExtendedQueryParser.Create(), CreateQueryExecutor(url, resource, dataStore)) @@ -36,7 +36,7 @@ public CollectionResourceQueryable(string url, string resource, IDataStore dataS this.DataStore = dataStore; } - // This constructor is called internally by LINQ + // (This constructor is called internally by LINQ) public CollectionResourceQueryable(IQueryProvider provider, Expression expression) : base(provider, expression) { @@ -72,9 +72,9 @@ int ICollectionResourceQueryable.Size } } - private static IQueryExecutor CreateQueryExecutor(string url, string resource, IDataStore dataStore) + private static IAsyncQueryExecutor CreateQueryExecutor(string url, string resource, IDataStore dataStore) { - return new CollectionResourceQueryExecutor(url, resource, dataStore); + return new CollectionResourceQueryAsyncExecutor(url, resource, dataStore); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Linq/AsyncQueryableExtensions.cs b/Stormpath.SDK/Stormpath.SDK/Linq/AsyncQueryableExtensions.cs new file mode 100644 index 00000000..fc8ace49 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Linq/AsyncQueryableExtensions.cs @@ -0,0 +1,165 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Stormpath.SDK.Impl.Linq; +using Stormpath.SDK.Linq; + +// Placed in the base library namespace so that these extension methods are available without any extra usings +namespace Stormpath +{ + public static class AsyncQueryableExtensions + { + private static readonly MethodInfo First = GetMethod(nameof(Queryable.First)); + + private static readonly MethodInfo FirstOrDefault = GetMethod(nameof(Queryable.FirstOrDefault)); + + /// + /// Asynchronously returns the first element of a sequence. + /// + /// + /// Multiple active operations on the same context instance are not supported. Use 'await' to ensure + /// that any asynchronous operations have completed before calling another method on this context. + /// + /// + /// The type of the elements of . + /// + /// + /// An to return the first element of. + /// + /// + /// A to observe while waiting for the task to complete. + /// + /// + /// A task that represents the asynchronous operation. + /// The task result contains the first element in . + /// + public static Task FirstAsync(this IAsyncQueryable source, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (source == null) + throw new ArgumentException("Source cannot be null."); + + return ExecuteAsync(First, source, cancellationToken); + } + + /// + /// Asynchronously returns the first element of a sequence, or a default value if the sequence contains no elements. + /// + /// + /// Multiple active operations on the same context instance are not supported. Use 'await' to ensure + /// that any asynchronous operations have completed before calling another method on this context. + /// + /// + /// The type of the elements of . + /// + /// + /// An to return the first element of. + /// + /// + /// A to observe while waiting for the task to complete. + /// + /// + /// A task that represents the asynchronous operation. + /// The task result contains default ( ) if + /// is empty; otherwise, the first element in . + /// + public static Task FirstOrDefaultAsync(this IAsyncQueryable source, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (source == null) + throw new ArgumentException("Source cannot be null."); + + return ExecuteAsync(FirstOrDefault, source, cancellationToken); + } + + private static Task ExecuteAsync( + MethodInfo operatorMethodInfo, + IAsyncQueryable source, + CancellationToken cancellationToken = default(CancellationToken)) + { + var provider = source.AsyncProvider; + + if (provider != null) + { + if (operatorMethodInfo.IsGenericMethod) + { + operatorMethodInfo = operatorMethodInfo.MakeGenericMethod(typeof(TSource)); + } + + return provider.ExecuteAsync( + Expression.Call(null, operatorMethodInfo, source.Expression), + cancellationToken); + } + + throw new InvalidOperationException("The underlying query provider does not support asynchronous execution."); + } + + private static Task ExecuteAsync( + MethodInfo operatorMethodInfo, + IAsyncQueryable source, + LambdaExpression expression, + CancellationToken cancellationToken = default(CancellationToken)) + => ExecuteAsync( + operatorMethodInfo, source, Expression.Quote(expression), cancellationToken); + + private static Task ExecuteAsync( + MethodInfo operatorMethodInfo, + IAsyncQueryable source, + Expression expression, + CancellationToken cancellationToken = default(CancellationToken)) + { + var provider = source.Provider as IAsyncQueryProvider; + + if (provider != null) + { + operatorMethodInfo + = operatorMethodInfo.GetGenericArguments().Length == 2 + ? operatorMethodInfo.MakeGenericMethod(typeof(TSource), typeof(TResult)) + : operatorMethodInfo.MakeGenericMethod(typeof(TSource)); + + return provider.ExecuteAsync( + Expression.Call( + null, + operatorMethodInfo, + new[] { source.Expression, expression }), + cancellationToken); + } + + throw new InvalidOperationException("The underlying query provider does not support asynchronous execution."); + } + + private static MethodInfo GetMethod( + string name, int parameterCount = 0, Func predicate = null) + => GetMethod( + name, + parameterCount, + mi => mi.ReturnType == typeof(TResult) + && (predicate == null || predicate(mi))); + + private static MethodInfo GetMethod( + string name, int parameterCount = 0, Func predicate = null) + => typeof(Queryable).GetTypeInfo().GetDeclaredMethods(name) + .Single(mi => mi.GetParameters().Length == parameterCount + 1 + && (predicate == null || predicate(mi))); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs b/Stormpath.SDK/Stormpath.SDK/Linq/CollectionResourceQueryableExtensions.cs similarity index 99% rename from Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs rename to Stormpath.SDK/Stormpath.SDK/Linq/CollectionResourceQueryableExtensions.cs index 8d65a379..2ecbc86a 100644 --- a/Stormpath.SDK/Stormpath.SDK/Resource/CollectionResourceQueryableExtensions.cs +++ b/Stormpath.SDK/Stormpath.SDK/Linq/CollectionResourceQueryableExtensions.cs @@ -21,7 +21,7 @@ using Stormpath.SDK.Resource; // Placed in the base library namespace so that these extension methods are available without any extra usings -namespace Stormpath.SDK +namespace Stormpath { public static class CollectionResourceQueryableFilterExtensions { diff --git a/Stormpath.SDK/Stormpath.SDK/Linq/IAsyncQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Linq/IAsyncQueryable.cs new file mode 100644 index 00000000..9bd2df19 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Linq/IAsyncQueryable.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; +using Stormpath.SDK.Impl.Linq; + +namespace Stormpath.SDK.Linq +{ + public interface IAsyncQueryable : IQueryable + { + IAsyncQueryProvider AsyncProvider { get; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs index b8b14969..194801d2 100644 --- a/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs +++ b/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs @@ -15,11 +15,11 @@ // limitations under the License. // -using System.Linq; +using Stormpath.SDK.Linq; namespace Stormpath.SDK.Resource { - public interface ICollectionResourceQueryable : IQueryable + public interface ICollectionResourceQueryable : IAsyncQueryable { int Offset { get; } diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs b/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs deleted file mode 100644 index d12058df..00000000 --- a/Stormpath.SDK/Stormpath.SDK/Resource/Shorthand.cs +++ /dev/null @@ -1,175 +0,0 @@ -// -// Copyright (c) 2015 Stormpath, Inc. -// -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; - -namespace Stormpath.SDK.Resource -{ - public struct Shorthand - { - private int year; - private int? month; - private int? day; - private int? hour; - private int? minute; - private int? second; - private TimeSpan offset; - - private Shorthand(int year, TimeSpan offset) - { - this.year = year; - this.month = null; - this.day = null; - this.hour = null; - this.minute = null; - this.second = null; - this.offset = offset; - } - - private Shorthand(int year, int month, TimeSpan offset) - { - this.year = year; - this.month = month; - this.day = null; - this.hour = null; - this.minute = null; - this.second = null; - this.offset = offset; - } - - private Shorthand(int year, int month, int day, TimeSpan offset) - { - this.year = year; - this.month = month; - this.day = day; - this.hour = null; - this.minute = null; - this.second = null; - this.offset = offset; - } - - private Shorthand(int year, int month, int day, int hour, TimeSpan offset) - { - this.year = year; - this.month = month; - this.day = day; - this.hour = hour; - this.minute = null; - this.second = null; - this.offset = offset; - } - - private Shorthand(int year, int month, int day, int hour, int minute, TimeSpan offset) - { - this.year = year; - this.month = month; - this.day = day; - this.hour = hour; - this.minute = minute; - this.second = null; - this.offset = offset; - } - - private Shorthand(int year, int month, int day, int hour, int minute, int second, TimeSpan offset) - { - this.year = year; - this.month = month; - this.day = day; - this.hour = hour; - this.minute = minute; - this.second = second; - this.offset = offset; - } - - public static implicit operator DateTimeOffset(Shorthand shorthand) - { - return new DateTimeOffset( - shorthand.year, - shorthand.month ?? 1, - shorthand.day ?? 1, - shorthand.hour ?? 0, - shorthand.minute ?? 0, - shorthand.second ?? 0, - shorthand.offset); - } - - private static TimeSpan GetLocalOffset() - { - return TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow); - } - - public static Shorthand Year(int year) - { - return Year(year, GetLocalOffset()); - } - - public static Shorthand Year(int year, TimeSpan offset) - { - return new Shorthand(year, offset); - } - - public static Shorthand Month(int year, int month) - { - return Month(year, month, GetLocalOffset()); - } - - public static Shorthand Month(int year, int month, TimeSpan offset) - { - return new Shorthand(year, month, offset); - } - - public static Shorthand Day(int year, int month, int day) - { - return Day(year, month, day, GetLocalOffset()); - } - - public static Shorthand Day(int year, int month, int day, TimeSpan offset) - { - return new Shorthand(year, month, day, offset); - } - - public static Shorthand Hour(int year, int month, int day, int hour) - { - return Hour(year, month, day, hour, GetLocalOffset()); - } - - public static Shorthand Hour(int year, int month, int day, int hour, TimeSpan offset) - { - return new Shorthand(year, month, day, hour, offset); - } - - public static Shorthand Minute(int year, int month, int day, int hour, int minute) - { - return Minute(year, month, day, hour, minute, GetLocalOffset()); - } - - public static Shorthand Minute(int year, int month, int day, int hour, int minute, TimeSpan offset) - { - return new Shorthand(year, month, day, hour, minute, offset); - } - - public static Shorthand Second(int year, int month, int day, int hour, int minute, int second) - { - return Second(year, month, day, hour, minute, second, GetLocalOffset()); - } - - public static Shorthand Second(int year, int month, int day, int hour, int minute, int second, TimeSpan offset) - { - return new Shorthand(year, month, day, hour, minute, second, offset); - } - } -} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index e0d99568..972ddcae 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -59,7 +59,7 @@ - + @@ -79,9 +79,13 @@ - + + + + + @@ -105,11 +109,12 @@ - + + + - diff --git a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs index 7504234d..0a8f5b58 100644 --- a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs +++ b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs @@ -27,9 +27,9 @@ public interface ITenantActions { Task CreateApplicationAsync(IApplication application); - Task GetApplicationsAsync(); + Task GetApplicationsAsync(); - IApplicationList GetApplications(); + IApplicationAsyncList GetApplications(); Task CreateDirectoryAsync(IDirectory directory); From d700a52ab82c137dd76ccc4ff901ae4cb85ac7ed Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Tue, 11 Aug 2015 11:53:29 -0700 Subject: [PATCH 031/238] Refactoring LINQ tests and async --- .../Impl/Linq/AsyncTests.cs | 2 +- .../Impl/Linq/ExpandExtensionTests.cs | 51 +++--- .../Impl/Linq/FilterExtensionTests.cs | 14 +- .../Impl/Linq/LimitTests.cs | 28 ++- .../Impl/Linq/LinqAssertExtensions.cs | 36 ++++ .../Impl/Linq/LinqTestHarness.cs | 9 +- .../Impl/Linq/OffsetTests.cs | 14 +- .../Impl/Linq/OrderByTests.cs | 28 ++- .../Impl/Linq/UnsupportedFilterTests.cs | 58 +++--- .../Impl/Linq/WhereTests.cs | 105 ++++++----- .../Impl/Linq/WithinExtensionTests.cs | 42 ++--- .../Stormpath.SDK.Tests.csproj | 1 + Stormpath.SDK/Stormpath.SDK.sln | 1 - .../{ => Impl}/DataStore/IDataStore.cs | 10 +- .../Impl/Linq/AsyncQueryableBase.cs | 1 - .../CollectionResourceQueryAsyncExecutor.cs | 28 ++- .../Resource/CollectionResourceQueryable.cs | 127 ++++++++++++-- .../Resource/CollectionResponsePageDto.cs | 34 ++++ .../Linq/AsyncQueryableExtensions.cs | 165 ------------------ .../Stormpath.SDK/Linq/IAsyncQueryable.cs | 5 +- .../Resource/ICollectionResourceQueryable.cs | 5 + .../Stormpath.SDK/Stormpath.SDK.csproj | 8 +- 22 files changed, 383 insertions(+), 389 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqAssertExtensions.cs rename Stormpath.SDK/Stormpath.SDK/{ => Impl}/DataStore/IDataStore.cs (77%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePageDto.cs delete mode 100644 Stormpath.SDK/Stormpath.SDK/Linq/AsyncQueryableExtensions.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/AsyncTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/AsyncTests.cs index 64339ff1..2a51c0c5 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/AsyncTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/AsyncTests.cs @@ -35,7 +35,7 @@ public async Task FirstAsync() var harness = LinqTestHarness.Create(url, resource); var applications = harness.Queryable; - var first = await applications.FirstAsync(); + IApplication first = null; // await applications.FirstAsync(); first.ShouldNotBe(null); } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs index 2ba1e939..cdac4509 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ExpandExtensionTests.cs @@ -35,11 +35,10 @@ public void Expand_one_link() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Expand(x => x.GetDirectoryAsync()) - .ToList(); + var query = harness.Queryable + .Expand(x => x.GetDirectoryAsync()); - harness.WasCalledWithArguments("expand=directory"); + query.GeneratedArgumentsWere(url, resource, "expand=directory"); } [TestMethod] @@ -48,12 +47,11 @@ public void Expand_multiple_links() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable + var query = harness.Queryable .Expand(x => x.GetDirectoryAsync()) - .Expand(x => x.GetTenantAsync()) - .ToList(); + .Expand(x => x.GetTenantAsync()); - harness.WasCalledWithArguments("expand=directory,tenant"); + query.GeneratedArgumentsWere(url, resource, "expand=directory,tenant"); } [TestMethod] @@ -62,11 +60,10 @@ public void Expand_collection_query_with_offset() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Expand(x => x.GetGroupsAsync(), offset: 10) - .ToList(); + var query = harness.Queryable + .Expand(x => x.GetGroupsAsync(), offset: 10); - harness.WasCalledWithArguments("expand=groups(offset:10)"); + query.GeneratedArgumentsWere(url, resource, "expand=groups(offset:10)"); } [TestMethod] @@ -75,11 +72,10 @@ public void Expand_collection_query_with_limit() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Expand(x => x.GetGroupsAsync(), limit: 20) - .ToList(); + var query = harness.Queryable + .Expand(x => x.GetGroupsAsync(), limit: 20); - harness.WasCalledWithArguments("expand=groups(limit:20)"); + query.GeneratedArgumentsWere(url, resource, "expand=groups(limit:20)"); } [TestMethod] @@ -88,11 +84,10 @@ public void Expand_collection_query_with_both_parameters() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Expand(x => x.GetGroupsAsync(), 5, 15) - .ToList(); + var query = harness.Queryable + .Expand(x => x.GetGroupsAsync(), 5, 15); - harness.WasCalledWithArguments("expand=groups(offset:5,limit:15)"); + query.GeneratedArgumentsWere(url, resource, "expand=groups(offset:5,limit:15)"); } [TestMethod] @@ -101,13 +96,12 @@ public void Expand_all_the_things() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable + var query = harness.Queryable .Expand(x => x.GetTenantAsync()) .Expand(x => x.GetGroupsAsync(), 10, 20) - .Expand(x => x.GetDirectoryAsync()) - .ToList(); + .Expand(x => x.GetDirectoryAsync()); - harness.WasCalledWithArguments("expand=tenant,groups(offset:10,limit:20),directory"); + query.GeneratedArgumentsWere(url, resource, "expand=tenant,groups(offset:10,limit:20),directory"); } [TestMethod] @@ -118,7 +112,8 @@ public void Expand_throws_if_used_on_an_attribute() Should.Throw(() => { - harness.Queryable.Expand(x => x.Email).ToList(); + var query = harness.Queryable.Expand(x => x.Email); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -130,7 +125,8 @@ public void Expand_throws_if_parameters_are_supplied_for_link() Should.Throw(() => { - harness.Queryable.Expand(x => x.GetDirectoryAsync(), limit: 10).ToList(); + var query = harness.Queryable.Expand(x => x.GetDirectoryAsync(), limit: 10); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -142,7 +138,8 @@ public void Expand_throws_if_syntax_is_dumb() Should.Throw(() => { - harness.Queryable.Expand(x => x.GetTenantAsync().GetAwaiter()).ToList(); + var query = harness.Queryable.Expand(x => x.GetTenantAsync().GetAwaiter()); + query.GeneratedArgumentsWere(url, resource, ""); }); } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs index b1f611a2..846b9d6f 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FilterExtensionTests.cs @@ -35,12 +35,11 @@ public void Filter_with_simple_parameter() var harness = LinqTestHarness.Create(url, resource); // Act - harness.Queryable - .Filter("Joe") - .ToList(); + var query = harness.Queryable + .Filter("Joe"); // Assert - harness.WasCalledWithArguments("q=Joe"); + query.GeneratedArgumentsWere(url, resource, "q=Joe"); } [TestMethod] @@ -49,13 +48,12 @@ public void Filter_multiple_calls_are_LIFO() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable + var query = harness.Queryable .Filter("Joe") - .Filter("Joey") - .ToList(); + .Filter("Joey"); // Expected behavior: the last call will be kept - harness.WasCalledWithArguments("q=Joey"); + query.GeneratedArgumentsWere(url, resource, "q=Joey"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs index 06627186..8acfad84 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LimitTests.cs @@ -34,11 +34,10 @@ public void Take_with_constant_becomes_limit() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Take(10) - .ToList(); + var query = harness.Queryable + .Take(10); - harness.WasCalledWithArguments("limit=10"); + query.GeneratedArgumentsWere(url, resource, "limit=10"); } [TestMethod] @@ -48,11 +47,10 @@ public void Take_with_variable_becomes_limit() var harness = LinqTestHarness.Create(url, resource); var limit = 20; - harness.Queryable - .Take(limit) - .ToList(); + var query = harness.Queryable + .Take(limit); - harness.WasCalledWithArguments("limit=20"); + query.GeneratedArgumentsWere(url, resource, "limit=20"); } [TestMethod] @@ -62,11 +60,10 @@ public void Take_with_function_becomes_limit() var harness = LinqTestHarness.Create(url, resource); var limitFunc = new Func(() => 25); - harness.Queryable - .Take(limitFunc()) - .ToList(); + var query = harness.Queryable + .Take(limitFunc()); - harness.WasCalledWithArguments("limit=25"); + query.GeneratedArgumentsWere(url, resource, "limit=25"); } [TestMethod] @@ -75,12 +72,11 @@ public void Take_multiple_calls_are_LIFO() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Take(10).Take(5) - .ToList(); + var query = harness.Queryable + .Take(10).Take(5); // Expected behavior: the last call will be kept - harness.WasCalledWithArguments("limit=5"); + query.GeneratedArgumentsWere(url, resource, "limit=5"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqAssertExtensions.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqAssertExtensions.cs new file mode 100644 index 00000000..ba83f70a --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqAssertExtensions.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stormpath.SDK.Resource; + +namespace Stormpath.SDK.Tests.Impl.Linq +{ + public static class LinqAssertExtensions + { + public static void GeneratedArgumentsWere(this IQueryable queryable, string url, string resource, string arguments) + { + var resourceQueryable = queryable as ICollectionResourceQueryable; + if (resourceQueryable == null) + Assert.Fail("This queryable is not an ICollectionResourceQueryable."); + + resourceQueryable.CurrentHref.ShouldBe($"{url}/{resource}?{arguments}"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs index 41fd6f8e..0875a71c 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/LinqTestHarness.cs @@ -16,7 +16,8 @@ // using NSubstitute; -using Stormpath.SDK.DataStore; +using Shouldly; +using Stormpath.SDK.Impl.DataStore; using Stormpath.SDK.Impl.Resource; using Stormpath.SDK.Resource; @@ -25,7 +26,7 @@ namespace Stormpath.SDK.Tests.Impl.Linq public class LinqTestHarness where T : IResource { - public IDataStore DataStore { get; private set; } + internal IDataStore DataStore { get; private set; } public string Url { get; private set; } @@ -33,9 +34,9 @@ public class LinqTestHarness public ICollectionResourceQueryable Queryable { get; private set; } - public void WasCalledWithArguments(string arguments) + public void GeneratedArgumentsWere(string arguments) { - DataStore.Received().GetCollection($"{Url}/{Resource}?{arguments}"); + Queryable.CurrentHref.ShouldBe($"{Url}/{Resource}?{arguments}"); } public static LinqTestHarness Create(string url, string resource) diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs index ce76e810..5b2191bf 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OffsetTests.cs @@ -35,12 +35,11 @@ public void Filter_with_simple_parameter() var harness = LinqTestHarness.Create(url, resource); // Act - harness.Queryable - .Filter("Joe") - .ToList(); + var query = harness.Queryable + .Filter("Joe"); // Assert - harness.WasCalledWithArguments("q=Joe"); + query.GeneratedArgumentsWere(url, resource, "q=Joe"); } [TestMethod] @@ -49,13 +48,12 @@ public void Filter_multiple_calls_are_LIFO() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable + var query = harness.Queryable .Filter("Joe") - .Filter("Joey") - .ToList(); + .Filter("Joey"); // Expected behavior: the last call will be kept - harness.WasCalledWithArguments("q=Joey"); + query.GeneratedArgumentsWere(url, resource, "q=Joey"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs index 8e49ec9b..10e1bb9c 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderByTests.cs @@ -34,11 +34,10 @@ public void Skip_becomes_offset() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Skip(10) - .ToList(); + var query = harness.Queryable + .Skip(10); - harness.WasCalledWithArguments("offset=10"); + query.GeneratedArgumentsWere(url, resource, "offset=10"); } [TestMethod] @@ -48,11 +47,10 @@ public void Skip_with_variable_becomes_offset() var harness = LinqTestHarness.Create(url, resource); var offset = 20; - harness.Queryable - .Skip(offset) - .ToList(); + var query = harness.Queryable + .Skip(offset); - harness.WasCalledWithArguments("offset=20"); + query.GeneratedArgumentsWere(url, resource, "offset=20"); } [TestMethod] @@ -62,11 +60,10 @@ public void Skip_with_function_becomes_offset() var harness = LinqTestHarness.Create(url, resource); var offsetFunc = new Func(() => 25); - harness.Queryable - .Skip(offsetFunc()) - .ToList(); + var query = harness.Queryable + .Skip(offsetFunc()); - harness.WasCalledWithArguments("offset=25"); + query.GeneratedArgumentsWere(url, resource, "offset=25"); } [TestMethod] @@ -75,12 +72,11 @@ public void Skip_multiple_calls_are_LIFO() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Skip(10).Skip(5) - .ToList(); + var query = harness.Queryable + .Skip(10).Skip(5); // Expected behavior: the last call will be kept - harness.WasCalledWithArguments("offset=5"); + query.GeneratedArgumentsWere(url, resource, "offset=5"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs index f26c8c48..cf18d363 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/UnsupportedFilterTests.cs @@ -74,7 +74,8 @@ public void Cast_is_unsupported() Should.Throw(() => { - harness.Queryable.Cast().ToList(); + var query = harness.Queryable.Cast(); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -86,7 +87,8 @@ public void Concat_is_unsupported() Should.Throw(() => { - harness.Queryable.Concat(Enumerable.Empty()).ToList(); + var query = harness.Queryable.Concat(Enumerable.Empty()); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -110,7 +112,8 @@ public void Distinct_is_unsupported() Should.Throw(() => { - harness.Queryable.Distinct().ToList(); + var query = harness.Queryable.Distinct(); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -122,7 +125,8 @@ public void Except_is_unsupported() Should.Throw(() => { - harness.Queryable.Except(Enumerable.Empty()).ToList(); + var query = harness.Queryable.Except(Enumerable.Empty()); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -134,7 +138,8 @@ public void GroupBy_is_unsupported() Should.Throw(() => { - harness.Queryable.GroupBy(x => x.Email).ToList(); + var query = harness.Queryable.GroupBy(x => x.Email); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -146,11 +151,11 @@ public void GroupJoin_clause_is_unsupported() Should.Throw(() => { - harness.Queryable.GroupJoin(Enumerable.Empty(), + var query = harness.Queryable.GroupJoin(Enumerable.Empty(), outer => outer.Email, inner => inner.Username, - (outer, results) => new { outer.CreatedAt, results }) - .ToList(); + (outer, results) => new { outer.CreatedAt, results }); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -162,7 +167,8 @@ public void Intersect_is_unsupported() Should.Throw(() => { - harness.Queryable.Intersect(Enumerable.Empty()).ToList(); + var query = harness.Queryable.Intersect(Enumerable.Empty()); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -174,10 +180,11 @@ public void Join_clause_is_unsupported() Should.Throw(() => { - harness.Queryable.Join(Enumerable.Empty(), + var query = harness.Queryable.Join(Enumerable.Empty(), outer => outer.Email, inner => inner.Username, - (outer, inner) => outer.CreatedAt).ToList(); + (outer, inner) => outer.CreatedAt); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -230,7 +237,8 @@ public void OfType_is_unsupported() Should.Throw(() => { - harness.Queryable.OfType().ToList(); + var query = harness.Queryable.OfType(); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -242,7 +250,8 @@ public void Reverse_is_unsupported() Should.Throw(() => { - harness.Queryable.Reverse().ToList(); + var query = harness.Queryable.Reverse(); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -254,9 +263,9 @@ public void Select_is_unsupported() Should.Throw(() => { - harness.Queryable - .Select(x => x.Email) - .ToList(); + var query = harness.Queryable + .Select(x => x.Email); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -268,7 +277,8 @@ public void SelectMany_is_unsupported() Should.Throw(() => { - harness.Queryable.SelectMany(x => x.Email).ToList(); + var query = harness.Queryable.SelectMany(x => x.Email); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -292,7 +302,8 @@ public void SkipWhile_is_unsupported() Should.Throw(() => { - harness.Queryable.SkipWhile(x => x.Email == "foobar").ToList(); + var query = harness.Queryable.SkipWhile(x => x.Email == "foobar"); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -316,7 +327,8 @@ public void TakeWhile_is_unsupported() Should.Throw(() => { - harness.Queryable.TakeWhile(x => x.Email == "foobar").ToList(); + var query = harness.Queryable.TakeWhile(x => x.Email == "foobar"); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -328,7 +340,8 @@ public void Union_is_unsupported() Should.Throw(() => { - harness.Queryable.Union(Enumerable.Empty()).ToList(); + var query = harness.Queryable.Union(Enumerable.Empty()); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -340,8 +353,9 @@ public void Zip_is_unsupported() Should.Throw(() => { - harness.Queryable.Zip(Enumerable.Empty(), - (first, second) => first.Email == second.Email).ToList(); + var query = harness.Queryable.Zip(Enumerable.Empty(), + (first, second) => first.Email == second.Email); + query.GeneratedArgumentsWere(url, resource, ""); }); } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs index c86a09be..1038fa0d 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WhereTests.cs @@ -37,12 +37,14 @@ public void Where_throws_for_constant() Should.Throw(() => { - harness.Queryable.Where(x => true).ToList(); + var query = harness.Queryable.Where(x => true); + query.GeneratedArgumentsWere(url, resource, ""); }); Should.Throw(() => { - harness.Queryable.Where(x => false).ToList(); + var query = harness.Queryable.Where(x => false); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -54,7 +56,8 @@ public void Where_throws_for_unsupported_comparison_operators() Should.Throw(() => { - harness.Queryable.Where(x => x.Email != "foo").ToList(); + var query = harness.Queryable.Where(x => x.Email != "foo"); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -66,12 +69,14 @@ public void Where_throws_for_more_complex_overloads_of_helper_methods() Should.Throw(() => { - harness.Queryable.Where(x => x.Email.Equals("bar", StringComparison.CurrentCulture)).ToList(); + var query = harness.Queryable.Where(x => x.Email.Equals("bar", StringComparison.CurrentCulture)); + query.GeneratedArgumentsWere(url, resource, ""); }); Should.Throw(() => { - harness.Queryable.Where(x => x.Email.StartsWith("foo", StringComparison.OrdinalIgnoreCase)).ToList(); + var query = harness.Queryable.Where(x => x.Email.StartsWith("foo", StringComparison.OrdinalIgnoreCase)); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -83,7 +88,8 @@ public void Where_throws_for_unsupported_helper_methods() Should.Throw(() => { - harness.Queryable.Where(x => x.Email.ToUpper() == "FOO").ToList(); + var query = harness.Queryable.Where(x => x.Email.ToUpper() == "FOO"); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -95,7 +101,8 @@ public void Where_throws_for_binary_or() Should.Throw(() => { - harness.Queryable.Where(x => x.Email == "foo" || x.Email == "bar").ToList(); + var query = harness.Queryable.Where(x => x.Email == "foo" || x.Email == "bar"); + query.GeneratedArgumentsWere(url, resource, ""); }); } @@ -105,11 +112,10 @@ public void Where_attribute_equals() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.Email == "tk421@deathstar.co") - .ToList(); + var query = harness.Queryable + .Where(x => x.Email == "tk421@deathstar.co"); - harness.WasCalledWithArguments("email=tk421@deathstar.co"); + query.GeneratedArgumentsWere(url, resource, "email=tk421@deathstar.co"); } [TestMethod] @@ -118,11 +124,10 @@ public void Where_attribute_equals_using_helper_method() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.Email.Equals("tk421@deathstar.co")) - .ToList(); + var query = harness.Queryable + .Where(x => x.Email.Equals("tk421@deathstar.co")); - harness.WasCalledWithArguments("email=tk421@deathstar.co"); + query.GeneratedArgumentsWere(url, resource, "email=tk421@deathstar.co"); } [TestMethod] @@ -131,11 +136,10 @@ public void Where_attribute_starts_with() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.Email.StartsWith("tk421")) - .ToList(); + var query = harness.Queryable + .Where(x => x.Email.StartsWith("tk421")); - harness.WasCalledWithArguments("email=tk421*"); + query.GeneratedArgumentsWere(url, resource, "email=tk421*"); } [TestMethod] @@ -144,11 +148,10 @@ public void Where_attribute_ends_with() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.Email.EndsWith("deathstar.co")) - .ToList(); + var query = harness.Queryable + .Where(x => x.Email.EndsWith("deathstar.co")); - harness.WasCalledWithArguments("email=*deathstar.co"); + query.GeneratedArgumentsWere(url, resource, "email=*deathstar.co"); } [TestMethod] @@ -157,11 +160,10 @@ public void Where_attribute_contains() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.Email.Contains("421")) - .ToList(); + var query = harness.Queryable + .Where(x => x.Email.Contains("421")); - harness.WasCalledWithArguments("email=*421*"); + query.GeneratedArgumentsWere(url, resource, "email=*421*"); } [TestMethod] @@ -170,11 +172,10 @@ public void Where_multiple_attributes_with_and() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.Email == "tk421@deathstar.co" && x.Username == "tk421") - .ToList(); + var query = harness.Queryable + .Where(x => x.Email == "tk421@deathstar.co" && x.Username == "tk421"); - harness.WasCalledWithArguments("email=tk421@deathstar.co&username=tk421"); + query.GeneratedArgumentsWere(url, resource, "email=tk421@deathstar.co&username=tk421"); } [TestMethod] @@ -183,12 +184,11 @@ public void Where_multiple_wheres() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable + var query = harness.Queryable .Where(x => x.Email == "tk421@deathstar.co") - .Where(x => x.Username.StartsWith("tk421")) - .ToList(); + .Where(x => x.Username.StartsWith("tk421")); - harness.WasCalledWithArguments("email=tk421@deathstar.co&username=tk421*"); + query.GeneratedArgumentsWere(url, resource, "email=tk421@deathstar.co&username=tk421*"); } [TestMethod] @@ -198,11 +198,10 @@ public void Where_date_attribute_greater_than() var harness = LinqTestHarness.Create(url, resource); var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); - harness.Queryable - .Where(x => x.CreatedAt > testDate) - .ToList(); + var query = harness.Queryable + .Where(x => x.CreatedAt > testDate); - harness.WasCalledWithArguments("createdAt=(2015-01-01T06:00:00.000Z,]"); + query.GeneratedArgumentsWere(url, resource, "createdAt=(2015-01-01T06:00:00.000Z,]"); } [TestMethod] @@ -212,11 +211,10 @@ public void Where_date_attribute_greater_than_or_equalto() var harness = LinqTestHarness.Create(url, resource); var testDate = new DateTimeOffset(2015, 01, 01, 06, 00, 00, TimeSpan.Zero); - harness.Queryable - .Where(x => x.CreatedAt >= testDate) - .ToList(); + var query = harness.Queryable + .Where(x => x.CreatedAt >= testDate); - harness.WasCalledWithArguments("createdAt=[2015-01-01T06:00:00.000Z,]"); + query.GeneratedArgumentsWere(url, resource, "createdAt=[2015-01-01T06:00:00.000Z,]"); } [TestMethod] @@ -226,11 +224,10 @@ public void Where_date_attribute_less_than() var harness = LinqTestHarness.Create(url, resource); var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); - harness.Queryable - .Where(x => x.ModifiedAt < testDate) - .ToList(); + var query = harness.Queryable + .Where(x => x.ModifiedAt < testDate); - harness.WasCalledWithArguments("modifiedAt=[,2016-01-01T12:00:00.000Z)"); + query.GeneratedArgumentsWere(url, resource, "modifiedAt=[,2016-01-01T12:00:00.000Z)"); } [TestMethod] @@ -240,11 +237,10 @@ public void Where_date_attribute_less_than_or_equalto() var harness = LinqTestHarness.Create(url, resource); var testDate = new DateTimeOffset(2016, 01, 01, 12, 00, 00, TimeSpan.Zero); - harness.Queryable - .Where(x => x.ModifiedAt <= testDate) - .ToList(); + var query = harness.Queryable + .Where(x => x.ModifiedAt <= testDate); - harness.WasCalledWithArguments("modifiedAt=[,2016-01-01T12:00:00.000Z]"); + query.GeneratedArgumentsWere(url, resource, "modifiedAt=[,2016-01-01T12:00:00.000Z]"); } [TestMethod] @@ -255,11 +251,10 @@ public void Where_date_attribute_between() var testStartDate = new DateTimeOffset(2015, 01, 01, 00, 00, 00, TimeSpan.Zero); var testEndDate = new DateTimeOffset(2015, 12, 31, 23, 59, 59, TimeSpan.Zero); - harness.Queryable - .Where(x => x.CreatedAt > testStartDate && x.CreatedAt <= testEndDate) - .ToList(); + var query = harness.Queryable + .Where(x => x.CreatedAt > testStartDate && x.CreatedAt <= testEndDate); - harness.WasCalledWithArguments("createdAt=(2015-01-01T00:00:00.000Z,2015-12-31T23:59:59.000Z]"); + query.GeneratedArgumentsWere(url, resource, "createdAt=(2015-01-01T00:00:00.000Z,2015-12-31T23:59:59.000Z]"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs index 24abb1a8..1e51aba9 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/WithinExtensionTests.cs @@ -46,11 +46,10 @@ public void Where_date_using_shorthand_for_year() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.CreatedAt.Within(2015)) - .ToList(); + var query = harness.Queryable + .Where(x => x.CreatedAt.Within(2015)); - harness.WasCalledWithArguments("createdAt=2015"); + query.GeneratedArgumentsWere(url, resource, "createdAt=2015"); } [TestMethod] @@ -59,11 +58,10 @@ public void Where_date_using_shorthand_for_month() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.CreatedAt.Within(2015, 01)) - .ToList(); + var query = harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01)); - harness.WasCalledWithArguments("createdAt=2015-01"); + query.GeneratedArgumentsWere(url, resource, "createdAt=2015-01"); } [TestMethod] @@ -72,11 +70,10 @@ public void Where_date_using_shorthand_for_day() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.CreatedAt.Within(2015, 01, 01)) - .ToList(); + var query = harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01, 01)); - harness.WasCalledWithArguments("createdAt=2015-01-01"); + query.GeneratedArgumentsWere(url, resource, "createdAt=2015-01-01"); } [TestMethod] @@ -85,11 +82,10 @@ public void Where_date_using_shorthand_for_hour() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.CreatedAt.Within(2015, 01, 01, 12)) - .ToList(); + var query = harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01, 01, 12)); - harness.WasCalledWithArguments("createdAt=2015-01-01T12"); + query.GeneratedArgumentsWere(url, resource, "createdAt=2015-01-01T12"); } [TestMethod] @@ -98,11 +94,10 @@ public void Where_date_using_shorthand_for_minute() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30)) - .ToList(); + var query = harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30)); - harness.WasCalledWithArguments("createdAt=2015-01-01T12:30"); + query.GeneratedArgumentsWere(url, resource, "createdAt=2015-01-01T12:30"); } [TestMethod] @@ -111,11 +106,10 @@ public void Where_date_using_shorthand_for_second() { var harness = LinqTestHarness.Create(url, resource); - harness.Queryable - .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30, 31)) - .ToList(); + var query = harness.Queryable + .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30, 31)); - harness.WasCalledWithArguments("createdAt=2015-01-01T12:30:31"); + query.GeneratedArgumentsWere(url, resource, "createdAt=2015-01-01T12:30:31"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 052411b5..40363e56 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -71,6 +71,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK.sln b/Stormpath.SDK/Stormpath.SDK.sln index 4e05f367..4a2ce791 100644 --- a/Stormpath.SDK/Stormpath.SDK.sln +++ b/Stormpath.SDK/Stormpath.SDK.sln @@ -24,7 +24,6 @@ Global {3579A409-6C66-4181-8268-C851878E5E22}.Release|Any CPU.ActiveCfg = Release|Any CPU {3579A409-6C66-4181-8268-C851878E5E22}.Release|Any CPU.Build.0 = Release|Any CPU {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection diff --git a/Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs similarity index 77% rename from Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs index 4f30a636..e72d2fa8 100644 --- a/Stormpath.SDK/Stormpath.SDK/DataStore/IDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs @@ -15,15 +15,13 @@ // limitations under the License. // -using System.Collections.Generic; using System.Threading.Tasks; +using Stormpath.SDK.Impl.Resource; -namespace Stormpath.SDK.DataStore +namespace Stormpath.SDK.Impl.DataStore { - public interface IDataStore + internal interface IDataStore { - IEnumerable GetCollection(string href); - - Task> GetCollectionAsync(string href); + Task> GetCollectionAsync(string href); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryableBase.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryableBase.cs index 3d28e3c2..0d28b3f1 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryableBase.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/AsyncQueryableBase.cs @@ -19,7 +19,6 @@ using System.Linq.Expressions; using Remotion.Linq; using Remotion.Linq.Parsing.Structure; -using Stormpath.SDK.Linq; namespace Stormpath.SDK.Impl.Linq { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryAsyncExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryAsyncExecutor.cs index ffb5d7ed..9636bcaf 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryAsyncExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryAsyncExecutor.cs @@ -20,28 +20,28 @@ using System.Linq; using System.Threading.Tasks; using Remotion.Linq; -using Stormpath.SDK.DataStore; +using Stormpath.SDK.Impl.DataStore; namespace Stormpath.SDK.Impl.Linq { internal class CollectionResourceQueryAsyncExecutor : IAsyncQueryExecutor { - private readonly string url; - private readonly string resource; - private readonly IDataStore dataStore; - public CollectionResourceQueryAsyncExecutor(string url, string resource, IDataStore dataStore) { - this.url = url; - this.resource = resource; - this.dataStore = dataStore; + this.Url = url; + this.Resource = resource; + this.DataStore = dataStore; } + public string Url { get; private set; } + + public string Resource { get; private set; } + + public IDataStore DataStore { get; private set; } + public IEnumerable ExecuteCollection(QueryModel queryModel) { - var arguments = QueryModelToArguments(queryModel); - - return dataStore.GetCollection($"{url}/{resource}?{arguments}"); + throw new NotImplementedException(); } public Task ExecuteAsync(QueryModel queryModel) @@ -74,11 +74,7 @@ public T ExecuteSingle(QueryModel queryModel, bool returnDefaultWhenEmpty) public Task> ExecuteCollectionAsync(QueryModel queryModel) { - var model = CollectionResourceQueryModelVisitor.GenerateRequestModel(queryModel); - var arguments = CollectionResourceRequestModelCompiler.GetArguments(model); - var argumentString = string.Join("&", arguments); - - return dataStore.GetCollectionAsync($"{url}/{resource}?{argumentString}"); + throw new NotImplementedException(); } public T ExecuteScalarAsync(QueryModel queryModel) diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs index c4d37808..a8c02573 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs @@ -16,43 +16,77 @@ // using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; using Remotion.Linq; -using Stormpath.SDK.DataStore; +using Stormpath.SDK.Impl.DataStore; using Stormpath.SDK.Impl.Linq; using Stormpath.SDK.Impl.Linq.Parsing; +using Stormpath.SDK.Impl.Linq.RequestModel; +using Stormpath.SDK.Linq; using Stormpath.SDK.Resource; namespace Stormpath.SDK.Impl.Resource { internal class CollectionResourceQueryable : AsyncQueryableBase, ICollectionResourceQueryable { + private readonly Expression expression; + + private readonly IDataStore dataStore; + + private readonly string baseHref; + + private CollectionResourceRequestModel compiledModel = null; + + private bool retrievedAtLeastOnce = false; + + private int currentOffset; + + private int currentLimit; + + private int currentSize; + + private IEnumerable currentItems; + public CollectionResourceQueryable(string url, string resource, IDataStore dataStore) : base(ExtendedQueryParser.Create(), CreateQueryExecutor(url, resource, dataStore)) { - this.Url = url; - this.Resource = resource; - this.DataStore = dataStore; + this.baseHref = $"{url}/{resource}"; + this.dataStore = dataStore; } // (This constructor is called internally by LINQ) public CollectionResourceQueryable(IQueryProvider provider, Expression expression) : base(provider, expression) { - } + var relinqProvider = provider as DefaultQueryProvider; + var executor = relinqProvider?.Executor as CollectionResourceQueryAsyncExecutor; + if (relinqProvider == null || executor == null) + throw new InvalidOperationException("LINQ queries must start from a supported ICollectionResourceQueryable."); - internal string Url { get; private set; } - - internal string Resource { get; private set; } + this.baseHref = $"{executor.Url}/{executor.Resource}"; + this.dataStore = executor.DataStore; + this.expression = expression; + } - internal IDataStore DataStore { get; private set; } + // (This is used by Relinq) + private static IAsyncQueryExecutor CreateQueryExecutor(string url, string resource, IDataStore dataStore) + { + return new CollectionResourceQueryAsyncExecutor(url, resource, dataStore); + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements must appear in the correct order", Justification = "Grouping internal methods above")] int ICollectionResourceQueryable.Offset { get { - throw new NotImplementedException(); + if (!retrievedAtLeastOnce) + throw new InvalidOperationException("Call MoveNextAsync() first to retrieve the collection."); + + return currentOffset; } } @@ -60,7 +94,10 @@ int ICollectionResourceQueryable.Limit { get { - throw new NotImplementedException(); + if (!retrievedAtLeastOnce) + throw new InvalidOperationException("Call MoveNextAsync() first to retrieve the collection."); + + return currentLimit; } } @@ -68,13 +105,75 @@ int ICollectionResourceQueryable.Size { get { - throw new NotImplementedException(); + if (!retrievedAtLeastOnce) + throw new InvalidOperationException("Call MoveNextAsync() first to retrieve the collection."); + + return currentSize; } } - private static IAsyncQueryExecutor CreateQueryExecutor(string url, string resource, IDataStore dataStore) + IEnumerable ICollectionResourceQueryable.CurrentPage { - return new CollectionResourceQueryAsyncExecutor(url, resource, dataStore); + get + { + if (!retrievedAtLeastOnce) + throw new InvalidOperationException("Call MoveNextAsync() first to retrieve the collection."); + + return currentItems; // TODO ?? Enumerable.Empty ? + } + } + + string ICollectionResourceQueryable.CurrentHref + { + get + { + return GenerateRequestUrlFromModel(); + } + } + + async Task IAsyncQueryable.MoveNextAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!IsCompiled()) + CompileExpressionToRequestModel(); + + var url = GenerateRequestUrlFromModel(); + var result = await dataStore.GetCollectionAsync(url); + + if (!result.Items.Any()) + return false; + + this.currentOffset = result.Offset; + this.currentLimit = result.Limit; + this.currentSize = result.Size; + this.currentItems = result.Items; + + return true; + } + + private bool IsCompiled() + { + return this.compiledModel != null; + } + + private void CompileExpressionToRequestModel() + { + var model = ExtendedQueryParser.Create().GetParsedQuery(this.expression); + var visitor = new CollectionResourceQueryModelVisitor(); + visitor.VisitQueryModel(model); + this.compiledModel = visitor.ParsedModel; + } + + private string GenerateRequestUrlFromModel() + { + if (!IsCompiled()) + CompileExpressionToRequestModel(); + + var argumentList = CollectionResourceRequestModelCompiler.GetArguments(this.compiledModel); + var arguments = string.Join("&", argumentList); + + return $"{baseHref}?{arguments}"; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePageDto.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePageDto.cs new file mode 100644 index 00000000..b5df9824 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePageDto.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; + +namespace Stormpath.SDK.Impl.Resource +{ + internal class CollectionResponsePageDto + { + public string Href { get; set; } + + public int Offset { get; set; } + + public int Limit { get; set; } + + public int Size { get; set; } + + public List Items { get; set; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Linq/AsyncQueryableExtensions.cs b/Stormpath.SDK/Stormpath.SDK/Linq/AsyncQueryableExtensions.cs deleted file mode 100644 index fc8ace49..00000000 --- a/Stormpath.SDK/Stormpath.SDK/Linq/AsyncQueryableExtensions.cs +++ /dev/null @@ -1,165 +0,0 @@ -// -// Copyright (c) 2015 Stormpath, Inc. -// -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Stormpath.SDK.Impl.Linq; -using Stormpath.SDK.Linq; - -// Placed in the base library namespace so that these extension methods are available without any extra usings -namespace Stormpath -{ - public static class AsyncQueryableExtensions - { - private static readonly MethodInfo First = GetMethod(nameof(Queryable.First)); - - private static readonly MethodInfo FirstOrDefault = GetMethod(nameof(Queryable.FirstOrDefault)); - - /// - /// Asynchronously returns the first element of a sequence. - /// - /// - /// Multiple active operations on the same context instance are not supported. Use 'await' to ensure - /// that any asynchronous operations have completed before calling another method on this context. - /// - /// - /// The type of the elements of . - /// - /// - /// An to return the first element of. - /// - /// - /// A to observe while waiting for the task to complete. - /// - /// - /// A task that represents the asynchronous operation. - /// The task result contains the first element in . - /// - public static Task FirstAsync(this IAsyncQueryable source, - CancellationToken cancellationToken = default(CancellationToken)) - { - if (source == null) - throw new ArgumentException("Source cannot be null."); - - return ExecuteAsync(First, source, cancellationToken); - } - - /// - /// Asynchronously returns the first element of a sequence, or a default value if the sequence contains no elements. - /// - /// - /// Multiple active operations on the same context instance are not supported. Use 'await' to ensure - /// that any asynchronous operations have completed before calling another method on this context. - /// - /// - /// The type of the elements of . - /// - /// - /// An to return the first element of. - /// - /// - /// A to observe while waiting for the task to complete. - /// - /// - /// A task that represents the asynchronous operation. - /// The task result contains default ( ) if - /// is empty; otherwise, the first element in . - /// - public static Task FirstOrDefaultAsync(this IAsyncQueryable source, - CancellationToken cancellationToken = default(CancellationToken)) - { - if (source == null) - throw new ArgumentException("Source cannot be null."); - - return ExecuteAsync(FirstOrDefault, source, cancellationToken); - } - - private static Task ExecuteAsync( - MethodInfo operatorMethodInfo, - IAsyncQueryable source, - CancellationToken cancellationToken = default(CancellationToken)) - { - var provider = source.AsyncProvider; - - if (provider != null) - { - if (operatorMethodInfo.IsGenericMethod) - { - operatorMethodInfo = operatorMethodInfo.MakeGenericMethod(typeof(TSource)); - } - - return provider.ExecuteAsync( - Expression.Call(null, operatorMethodInfo, source.Expression), - cancellationToken); - } - - throw new InvalidOperationException("The underlying query provider does not support asynchronous execution."); - } - - private static Task ExecuteAsync( - MethodInfo operatorMethodInfo, - IAsyncQueryable source, - LambdaExpression expression, - CancellationToken cancellationToken = default(CancellationToken)) - => ExecuteAsync( - operatorMethodInfo, source, Expression.Quote(expression), cancellationToken); - - private static Task ExecuteAsync( - MethodInfo operatorMethodInfo, - IAsyncQueryable source, - Expression expression, - CancellationToken cancellationToken = default(CancellationToken)) - { - var provider = source.Provider as IAsyncQueryProvider; - - if (provider != null) - { - operatorMethodInfo - = operatorMethodInfo.GetGenericArguments().Length == 2 - ? operatorMethodInfo.MakeGenericMethod(typeof(TSource), typeof(TResult)) - : operatorMethodInfo.MakeGenericMethod(typeof(TSource)); - - return provider.ExecuteAsync( - Expression.Call( - null, - operatorMethodInfo, - new[] { source.Expression, expression }), - cancellationToken); - } - - throw new InvalidOperationException("The underlying query provider does not support asynchronous execution."); - } - - private static MethodInfo GetMethod( - string name, int parameterCount = 0, Func predicate = null) - => GetMethod( - name, - parameterCount, - mi => mi.ReturnType == typeof(TResult) - && (predicate == null || predicate(mi))); - - private static MethodInfo GetMethod( - string name, int parameterCount = 0, Func predicate = null) - => typeof(Queryable).GetTypeInfo().GetDeclaredMethods(name) - .Single(mi => mi.GetParameters().Length == parameterCount + 1 - && (predicate == null || predicate(mi))); - } -} diff --git a/Stormpath.SDK/Stormpath.SDK/Linq/IAsyncQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Linq/IAsyncQueryable.cs index 9bd2df19..86a68936 100644 --- a/Stormpath.SDK/Stormpath.SDK/Linq/IAsyncQueryable.cs +++ b/Stormpath.SDK/Stormpath.SDK/Linq/IAsyncQueryable.cs @@ -16,12 +16,13 @@ // using System.Linq; -using Stormpath.SDK.Impl.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Stormpath.SDK.Linq { public interface IAsyncQueryable : IQueryable { - IAsyncQueryProvider AsyncProvider { get; } + Task MoveNextAsync(CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs index 194801d2..456bf9bc 100644 --- a/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs +++ b/Stormpath.SDK/Stormpath.SDK/Resource/ICollectionResourceQueryable.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System.Collections.Generic; using Stormpath.SDK.Linq; namespace Stormpath.SDK.Resource @@ -26,5 +27,9 @@ public interface ICollectionResourceQueryable : IAsyncQueryable int Limit { get; } int Size { get; } + + IEnumerable CurrentPage { get; } + + string CurrentHref { get; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 972ddcae..08f99144 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -64,7 +64,6 @@ - @@ -73,6 +72,7 @@ + @@ -107,9 +107,9 @@ + - @@ -137,7 +137,9 @@ - + + + - \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.ruleset b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.ruleset deleted file mode 100644 index 2cd41792..00000000 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.ruleset +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/packages.config b/Stormpath.SDK/Stormpath.SDK.Tests/packages.config deleted file mode 100644 index 232a0064..00000000 --- a/Stormpath.SDK/Stormpath.SDK.Tests/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.sln b/Stormpath.SDK/Stormpath.SDK.sln index 4a2ce791..a56932d5 100644 --- a/Stormpath.SDK/Stormpath.SDK.sln +++ b/Stormpath.SDK/Stormpath.SDK.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK", "Stormpath.SDK\Stormpath.SDK.csproj", "{79A65C37-9DB1-413A-AC23-708404530295}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.Tests", "Stormpath.SDK.Tests\Stormpath.SDK.Tests.csproj", "{3579A409-6C66-4181-8268-C851878E5E22}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.Demo", "Stormpath.Demo\Stormpath.Demo.csproj", "{301DA33A-8F36-47C7-BCD1-B89725689B4C}" EndProject Global @@ -19,10 +17,6 @@ Global {79A65C37-9DB1-413A-AC23-708404530295}.Debug|Any CPU.Build.0 = Debug|Any CPU {79A65C37-9DB1-413A-AC23-708404530295}.Release|Any CPU.ActiveCfg = Release|Any CPU {79A65C37-9DB1-413A-AC23-708404530295}.Release|Any CPU.Build.0 = Release|Any CPU - {3579A409-6C66-4181-8268-C851878E5E22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3579A409-6C66-4181-8268-C851878E5E22}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3579A409-6C66-4181-8268-C851878E5E22}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3579A409-6C66-4181-8268-C851878E5E22}.Release|Any CPU.Build.0 = Release|Any CPU {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Release|Any CPU.Build.0 = Release|Any CPU From f9470af387553a8d64d20151219bbc15cfb1be75 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 13 Aug 2015 11:18:47 -0700 Subject: [PATCH 039/238] Add xunit project --- Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs | 19 +++++ .../Properties/AssemblyInfo.cs | 36 +++++++++ .../Stormpath.SDK.Tests.csproj | 80 +++++++++++++++++++ .../Stormpath.SDK.Tests/packages.config | 9 +++ Stormpath.SDK/Stormpath.SDK.sln | 6 ++ 5 files changed, 150 insertions(+) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Properties/AssemblyInfo.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/packages.config diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs new file mode 100644 index 00000000..e26f02b6 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs @@ -0,0 +1,19 @@ +using Xunit; + +namespace Stormpath.SDK.Tests +{ + public class XunitTester + { + [Fact] + public void AssertBool() + { + Assert.True(true); + } + + [Fact] + public void AssertString() + { + Assert.NotEqual("foo", "bar"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..1b564c30 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Stormpath.SDK.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stormpath.SDK.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("58442311-36b7-4db8-8655-29611a46f748")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj new file mode 100644 index 00000000..9febcefd --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -0,0 +1,80 @@ + + + + + + + Debug + AnyCPU + {58442311-36B7-4DB8-8655-29611A46F748} + Library + Properties + Stormpath.SDK.Tests + Stormpath.SDK.Tests + v4.5.2 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True + + + ..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/packages.config b/Stormpath.SDK/Stormpath.SDK.Tests/packages.config new file mode 100644 index 00000000..72d2faa0 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.sln b/Stormpath.SDK/Stormpath.SDK.sln index a56932d5..ac652552 100644 --- a/Stormpath.SDK/Stormpath.SDK.sln +++ b/Stormpath.SDK/Stormpath.SDK.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK", "Stormpath. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.Demo", "Stormpath.Demo\Stormpath.Demo.csproj", "{301DA33A-8F36-47C7-BCD1-B89725689B4C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.Tests", "Stormpath.SDK.Tests\Stormpath.SDK.Tests.csproj", "{58442311-36B7-4DB8-8655-29611A46F748}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,6 +22,10 @@ Global {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {301DA33A-8F36-47C7-BCD1-B89725689B4C}.Release|Any CPU.Build.0 = Release|Any CPU + {58442311-36B7-4DB8-8655-29611A46F748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58442311-36B7-4DB8-8655-29611A46F748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58442311-36B7-4DB8-8655-29611A46F748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58442311-36B7-4DB8-8655-29611A46F748}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 5cff409460be8cc743d92996bceaec648be8e10a Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 13 Aug 2015 11:40:21 -0700 Subject: [PATCH 040/238] Trying to get xunit VS runner working, no luck yet --- Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs | 2 +- Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj | 2 -- Stormpath.SDK/Stormpath.SDK.Tests/packages.config | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs index e26f02b6..e307aa41 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Class1.cs @@ -16,4 +16,4 @@ public void AssertString() Assert.NotEqual("foo", "bar"); } } -} +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 9febcefd..e566f3e3 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -1,6 +1,5 @@  - @@ -68,7 +67,6 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/packages.config b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/packages.config new file mode 100644 index 00000000..5dc65cb9 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 0f3bb6a6..f8f9c899 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -24,7 +24,7 @@ DEBUG;TRACE prompt 4 - stormpath.sdk.tests.ruleset + ..\Stormpath.SDK\Stormpath.SDK.ruleset pdbonly @@ -109,7 +109,6 @@ Designer - diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.ruleset b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.ruleset deleted file mode 100644 index bed6b1e4..00000000 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.ruleset +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.sln b/Stormpath.SDK/Stormpath.SDK.sln index ac652552..165acf9a 100644 --- a/Stormpath.SDK/Stormpath.SDK.sln +++ b/Stormpath.SDK/Stormpath.SDK.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.Demo", "Stormpath EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.Tests", "Stormpath.SDK.Tests\Stormpath.SDK.Tests.csproj", "{58442311-36B7-4DB8-8655-29611A46F748}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.Tests.Integration", "Stormpath.SDK.Tests.Integration\Stormpath.SDK.Tests.Integration.csproj", "{09B79235-D67F-4FCA-9991-5AE30838EECE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {58442311-36B7-4DB8-8655-29611A46F748}.Debug|Any CPU.Build.0 = Debug|Any CPU {58442311-36B7-4DB8-8655-29611A46F748}.Release|Any CPU.ActiveCfg = Release|Any CPU {58442311-36B7-4DB8-8655-29611A46F748}.Release|Any CPU.Build.0 = Release|Any CPU + {09B79235-D67F-4FCA-9991-5AE30838EECE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09B79235-D67F-4FCA-9991-5AE30838EECE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09B79235-D67F-4FCA-9991-5AE30838EECE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09B79235-D67F-4FCA-9991-5AE30838EECE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 3c5e96a0b8a449ddc4c91f7245b61c6680dc35f9 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 11:27:30 -0700 Subject: [PATCH 067/238] Fixed StyleCop warnings --- .../Stormpath.Demo/Stormpath.Demo.csproj | 9 ++++++ .../NamespaceExtensions.cs | 21 +++++++++++-- .../Properties/AssemblyInfo.cs | 30 ++++++++++++++----- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj b/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj index 3fd18da7..2aa2c375 100644 --- a/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj +++ b/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj @@ -41,6 +41,15 @@ + + ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll + True + + + ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll + True + + diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/NamespaceExtensions.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/NamespaceExtensions.cs index 58ec2af9..ba1f3b49 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/NamespaceExtensions.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/NamespaceExtensions.cs @@ -1,9 +1,24 @@ -using System; +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Stormpath.SDK.Tests.Integration { diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Properties/AssemblyInfo.cs index 35ff884f..9f299278 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Properties/AssemblyInfo.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Properties/AssemblyInfo.cs @@ -1,8 +1,24 @@ -using System.Reflection; -using System.Runtime.CompilerServices; +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Reflection; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Stormpath.SDK.Tests.Integration")] @@ -14,8 +30,8 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] @@ -25,11 +41,11 @@ // Version information for an assembly consists of the following four values: // // Major Version -// Minor Version +// Minor Version // Build Number // Revision // -// You can specify all the values or you can default the Build and Revision Numbers +// You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] From adbf11397a0cb0a48e007d705f150a5a7ba0f6d5 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 11:30:24 -0700 Subject: [PATCH 068/238] Fixing mono build error (framework 4.5.2) --- Stormpath.SDK/Stormpath.Demo/packages.config | 2 ++ .../Stormpath.SDK.Tests.Integration/Namespace_tests.cs | 4 ---- .../Stormpath.SDK.Tests.Integration.csproj | 3 ++- Stormpath.SDK/Stormpath.SDK/Impl/InternalFactory.cs | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Stormpath.SDK/Stormpath.Demo/packages.config b/Stormpath.SDK/Stormpath.Demo/packages.config index 7a42323d..a5aa4e81 100644 --- a/Stormpath.SDK/Stormpath.Demo/packages.config +++ b/Stormpath.SDK/Stormpath.Demo/packages.config @@ -1,4 +1,6 @@  + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Namespace_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Namespace_tests.cs index 4a676650..d758e342 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Namespace_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Namespace_tests.cs @@ -15,12 +15,8 @@ // limitations under the License. // -using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using Shouldly; using Xunit; diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj index b8e93261..a27a9e79 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj @@ -9,8 +9,9 @@ Properties Stormpath.SDK.Tests.Integration Stormpath.SDK.Tests.Integration - v4.5.2 + v4.5 512 + true diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/InternalFactory.cs b/Stormpath.SDK/Stormpath.SDK/Impl/InternalFactory.cs index 0014c191..3ab1502d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/InternalFactory.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/InternalFactory.cs @@ -15,7 +15,6 @@ // limitations under the License. // -using System; using Stormpath.SDK.Api; using Stormpath.SDK.Client; using Stormpath.SDK.Impl.DataStore; From 7c3afac62b4da654a2fa8d2b0c1f2672a05380d6 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 13:38:17 -0700 Subject: [PATCH 069/238] Adding RequestAuthenticator factory --- ...efaultRequestAuthenticatorFactory_tests.cs | 58 +++++++++++++++++++ .../Stormpath.SDK.Tests.csproj | 1 + .../Client/AuthenticationScheme.cs | 3 +- Stormpath.SDK/Stormpath.SDK/Client/IClient.cs | 3 +- .../Impl/Client/DefaultClient.cs | 16 ++++- .../BasicRequestAuthenticator.cs | 4 +- .../DefaultRequestAuthenticatorFactory.cs | 36 ++++++++++++ .../Authentication/IRequestAuthenticator.cs | 3 +- .../IRequestAuthenticatorFactory.cs | 26 +++++++++ .../RequestAuthenticationException.cs | 35 +++++++++++ .../SAuthc1RequestAuthenticator.cs | 4 +- .../Impl/Http/NetHttpRequestExecutor.cs | 6 ++ .../Impl/Http/RequestException.cs | 35 +++++++++++ .../Stormpath.SDK/Stormpath.SDK.csproj | 4 ++ 14 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/DefaultRequestAuthenticatorFactory_tests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/DefaultRequestAuthenticatorFactory.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/IRequestAuthenticatorFactory.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/RequestAuthenticationException.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Http/RequestException.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/DefaultRequestAuthenticatorFactory_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/DefaultRequestAuthenticatorFactory_tests.cs new file mode 100644 index 00000000..2a125e2d --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/DefaultRequestAuthenticatorFactory_tests.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Shouldly; +using Stormpath.SDK.Client; +using Stormpath.SDK.Impl.Http.Authentication; +using Xunit; + +namespace Stormpath.SDK.Tests.Impl.Authentication +{ + public class DefaultRequestAuthenticatorFactory_tests + { + private readonly IRequestAuthenticatorFactory factory; + + public DefaultRequestAuthenticatorFactory_tests() + { + factory = new DefaultRequestAuthenticatorFactory(); + } + + [Fact] + public void Returns_SAuthc1_authenticator_for_null_scheme() + { + var authenticator = factory.Create(null); + + authenticator.ShouldBeOfType(); + } + + [Fact] + public void Returns_SAuthc1_authenticator() + { + var authenticator = factory.Create(AuthenticationScheme.SAuthc1); + + authenticator.ShouldBeOfType(); + } + + [Fact] + public void Returns_Basic_authenticator() + { + var authenticator = factory.Create(AuthenticationScheme.Basic); + + authenticator.ShouldBeOfType(); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index f8f9c899..c79161bc 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -79,6 +79,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Client/AuthenticationScheme.cs b/Stormpath.SDK/Stormpath.SDK/Client/AuthenticationScheme.cs index 5189f3ce..26783be4 100644 --- a/Stormpath.SDK/Stormpath.SDK/Client/AuthenticationScheme.cs +++ b/Stormpath.SDK/Stormpath.SDK/Client/AuthenticationScheme.cs @@ -21,12 +21,11 @@ namespace Stormpath.SDK.Client { + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields must be private", Justification = "")] public sealed class AuthenticationScheme : Enumeration { -#pragma warning disable SA1401 // Fields must be private public static AuthenticationScheme Basic = new AuthenticationScheme(0, "BASIC", typeof(BasicRequestAuthenticator)); public static AuthenticationScheme SAuthc1 = new AuthenticationScheme(1, "SAUTHC1", typeof(SAuthc1RequestAuthenticator)); -#pragma warning restore SA1401 // Fields must be private private readonly Type authenticatorType; diff --git a/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs b/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs index 81f8b88a..7fd6e023 100644 --- a/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Client/IClient.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System.Threading; using System.Threading.Tasks; using Stormpath.SDK.Tenant; @@ -28,6 +29,6 @@ public interface IClient : ITenantActions int ConnectionTimeout { get; } - Task GetCurrentTenantAsync(); + Task GetCurrentTenantAsync(CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs index 4ee126b9..44b0a4d0 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs @@ -16,11 +16,13 @@ // using System; +using System.Threading; using System.Threading.Tasks; using Stormpath.SDK.Api; using Stormpath.SDK.Application; using Stormpath.SDK.Client; using Stormpath.SDK.Impl.DataStore; +using Stormpath.SDK.Impl.Extensions; using Stormpath.SDK.Tenant; namespace Stormpath.SDK.Impl.Client @@ -28,6 +30,7 @@ namespace Stormpath.SDK.Impl.Client internal sealed class DefaultClient : IClient { private IInternalDataStore dataStore; + private string currentTenantHref; public DefaultClient(IClientApiKey apiKey, string baseUrl, AuthenticationScheme authenticationScheme, int connectionTimeout) { @@ -44,6 +47,8 @@ public DefaultClient(IClientApiKey apiKey, string baseUrl, AuthenticationScheme this.dataStore = factory.CreateDataStore(requestExecutor, baseUrl); } + private string CurrentTenantHref => currentTenantHref.Nullable() ?? "tenants/current"; + AuthenticationScheme IClient.AuthenticationScheme { get { return this.dataStore.RequestExecutor.AuthenticationScheme; } @@ -70,9 +75,16 @@ IApplicationAsyncList ITenantActions.GetApplications() throw new NotImplementedException(); } - Task IClient.GetCurrentTenantAsync() + async Task IClient.GetCurrentTenantAsync(CancellationToken cancellationToken) { - throw new NotImplementedException(); + var tenant = await dataStore + .GetResourceAsync(this.CurrentTenantHref, cancellationToken) + .ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + this.currentTenantHref = tenant.Href; + + return tenant; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs index 5bc5e912..989424c7 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs @@ -16,15 +16,15 @@ // using System; +using System.Net.Http; using Stormpath.SDK.Api; namespace Stormpath.SDK.Impl.Http.Authentication { internal sealed class BasicRequestAuthenticator : IRequestAuthenticator { - void IRequestAuthenticator.Authenticate(IRequest request, IClientApiKey apiKey) + void IRequestAuthenticator.Authenticate(HttpRequestMessage request, IClientApiKey apiKey) { - // TODO throw new NotImplementedException(); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/DefaultRequestAuthenticatorFactory.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/DefaultRequestAuthenticatorFactory.cs new file mode 100644 index 00000000..e5a84ca4 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/DefaultRequestAuthenticatorFactory.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Stormpath.SDK.Client; + +namespace Stormpath.SDK.Impl.Http.Authentication +{ + internal sealed class DefaultRequestAuthenticatorFactory : IRequestAuthenticatorFactory + { + IRequestAuthenticator IRequestAuthenticatorFactory.Create(AuthenticationScheme scheme) + { + if (scheme == null || + scheme == AuthenticationScheme.SAuthc1) + return new SAuthc1RequestAuthenticator(); + + if (scheme == AuthenticationScheme.Basic) + return new BasicRequestAuthenticator(); + + throw new RequestAuthenticationException("Unknown authentication scheme."); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/IRequestAuthenticator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/IRequestAuthenticator.cs index 67376a23..ddc8f524 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/IRequestAuthenticator.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/IRequestAuthenticator.cs @@ -15,12 +15,13 @@ // limitations under the License. // +using System.Net.Http; using Stormpath.SDK.Api; namespace Stormpath.SDK.Impl.Http.Authentication { internal interface IRequestAuthenticator { - void Authenticate(IRequest request, IClientApiKey apiKey); + void Authenticate(HttpRequestMessage request, IClientApiKey apiKey); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/IRequestAuthenticatorFactory.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/IRequestAuthenticatorFactory.cs new file mode 100644 index 00000000..badcec20 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/IRequestAuthenticatorFactory.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Stormpath.SDK.Client; + +namespace Stormpath.SDK.Impl.Http.Authentication +{ + internal interface IRequestAuthenticatorFactory + { + IRequestAuthenticator Create(AuthenticationScheme scheme); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/RequestAuthenticationException.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/RequestAuthenticationException.cs new file mode 100644 index 00000000..f1098c07 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/RequestAuthenticationException.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Stormpath.SDK.Impl.Http.Authentication +{ + [Serializable] + internal sealed class RequestAuthenticationException : RequestException + { + public RequestAuthenticationException(string message) + : base(message) + { + } + + public RequestAuthenticationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs index c9e95a49..83b2c9f1 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs @@ -16,14 +16,14 @@ // using System; +using System.Net.Http; using Stormpath.SDK.Api; namespace Stormpath.SDK.Impl.Http.Authentication { internal sealed class SAuthc1RequestAuthenticator : IRequestAuthenticator { - // TODO - void IRequestAuthenticator.Authenticate(IRequest request, IClientApiKey apiKey) + void IRequestAuthenticator.Authenticate(HttpRequestMessage request, IClientApiKey apiKey) { throw new NotImplementedException(); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs index 2d7c93dd..498f5140 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs @@ -41,12 +41,18 @@ public NetHttpRequestExecutor(IClientApiKey apiKey, AuthenticationScheme authent this.connectionTimeout = connectionTimeout; client = new HttpClient(); + SetupClient(); } AuthenticationScheme IRequestExecutor.AuthenticationScheme => authScheme; int IRequestExecutor.ConnectionTimeout => connectionTimeout; + private void SetupClient() + { + client.Timeout = TimeSpan.FromMilliseconds(connectionTimeout); + } + Task IRequestExecutor.GetAsync(string href, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/RequestException.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/RequestException.cs new file mode 100644 index 00000000..244b88a9 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/RequestException.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Stormpath.SDK.Impl.Http +{ + [Serializable] + internal class RequestException : ApplicationException + { + public RequestException(string message) + : base(message) + { + } + + public RequestException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 2768cf33..dfda7e37 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -85,6 +85,8 @@ + + @@ -103,6 +105,8 @@ + + From 201180bb04ce932dbf743258021ba002bcbd0036 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 14:12:20 -0700 Subject: [PATCH 070/238] Basic request authentication --- .../BasicRequestAuthenticator_tests.cs | 66 +++++++++++++++++++ .../Impl/Extensions_tests.cs | 44 ++++++++++++- .../Stormpath.SDK.Tests.csproj | 1 + .../Impl/Extensions/StringExtensions.cs | 19 ++++++ .../BasicRequestAuthenticator.cs | 13 +++- .../Stormpath.SDK/Impl/Utility/Iso8601.cs | 6 +- 6 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/BasicRequestAuthenticator_tests.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/BasicRequestAuthenticator_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/BasicRequestAuthenticator_tests.cs new file mode 100644 index 00000000..701cadb6 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/BasicRequestAuthenticator_tests.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using System.Net.Http; +using System.Text; +using Shouldly; +using Stormpath.SDK.Api; +using Stormpath.SDK.Impl.Api; +using Stormpath.SDK.Impl.Extensions; +using Stormpath.SDK.Impl.Http.Authentication; +using Stormpath.SDK.Impl.Utility; +using Xunit; + +namespace Stormpath.SDK.Tests.Impl.Authentication +{ + public class BasicRequestAuthenticator_tests + { + private readonly IRequestAuthenticator authenticator; + private readonly IClientApiKey apiKey; + + private readonly string fakeApiKeyId = "foo-api-key"; + private readonly string fakeApiKeySecret = "super-secret!1"; + + public BasicRequestAuthenticator_tests() + { + authenticator = new BasicRequestAuthenticator(); + apiKey = new ClientApiKey(fakeApiKeyId, fakeApiKeySecret); + } + + [Fact] + public void Authenticates_request_with_basic_scheme() + { + var myRequest = new HttpRequestMessage(); + + authenticator.Authenticate(myRequest, apiKey); + + // X-Stormpath-Date -> now in UTC + var XStormpathDateHeader = Iso8601.Parse(myRequest.Headers.GetValues("X-Stormpath-Date").Single()); + XStormpathDateHeader.Year.ShouldBe(DateTimeOffset.UtcNow.Year); + XStormpathDateHeader.Month.ShouldBe(DateTimeOffset.UtcNow.Month); + XStormpathDateHeader.Day.ShouldBe(DateTimeOffset.UtcNow.Day); + XStormpathDateHeader.Offset.ShouldBe(TimeSpan.Zero); + + // Authorization -> Basic [base64 stuff] + var authenticationHeader = myRequest.Headers.Authorization; + authenticationHeader.Scheme.ShouldBe("Basic"); + authenticationHeader.Parameter.FromBase64(Encoding.UTF8).ShouldBe($"{fakeApiKeyId}:{fakeApiKeySecret}"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Extensions_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Extensions_tests.cs index 832f6cb8..b8f131ad 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Extensions_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Extensions_tests.cs @@ -23,7 +23,7 @@ namespace Stormpath.SDK.Tests.Impl { public class Extensions_tests { - public class Nullable_string + public class String_Nullable { [Fact] public void Returns_null_when_string_is_null() @@ -43,5 +43,47 @@ public void Returns_string() "foobar".Nullable().ShouldBe("foobar"); } } + + public class String_ToBase64 + { + [Fact] + public void Returns_null_when_string_is_null() + { + ((string)null).ToBase64(System.Text.Encoding.UTF8).ShouldBe(null); + } + + [Fact] + public void Returns_empty_when_string_is_empty() + { + string.Empty.ToBase64(System.Text.Encoding.UTF8).ShouldBe(string.Empty); + } + + [Fact] + public void Returns_Zm9vYmFy_for_foobar() + { + "foobar".ToBase64(System.Text.Encoding.UTF8).ShouldBe("Zm9vYmFy"); + } + } + + public class String_FromBase64 + { + [Fact] + public void Returns_null_when_string_is_null() + { + ((string)null).FromBase64(System.Text.Encoding.UTF8).ShouldBe(null); + } + + [Fact] + public void Returns_empty_when_string_is_empty() + { + string.Empty.FromBase64(System.Text.Encoding.UTF8).ShouldBe(string.Empty); + } + + [Fact] + public void Returns_Zm9vYmFy_for_foobar() + { + "Zm9vYmFy".FromBase64(System.Text.Encoding.UTF8).ShouldBe("foobar"); + } + } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index c79161bc..37e4d39a 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -79,6 +79,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs index ebde2cfe..720bef3d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs @@ -15,6 +15,9 @@ // limitations under the License. // +using System; +using System.Text; + namespace Stormpath.SDK.Impl.Extensions { internal static class StringExtensions @@ -25,5 +28,21 @@ public static string Nullable(this string source) return null; return source; } + + public static string ToBase64(this string source, Encoding encoding) + { + if (source == null) + return null; + + return Convert.ToBase64String(encoding.GetBytes(source)); + } + + public static string FromBase64(this string base64Source, Encoding encoding) + { + if (base64Source == null) + return null; + + return encoding.GetString(Convert.FromBase64String(base64Source)); + } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs index 989424c7..97885e90 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs @@ -17,15 +17,26 @@ using System; using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; using Stormpath.SDK.Api; +using Stormpath.SDK.Impl.Extensions; +using Stormpath.SDK.Impl.Utility; namespace Stormpath.SDK.Impl.Http.Authentication { internal sealed class BasicRequestAuthenticator : IRequestAuthenticator { + private static readonly string StormpathDateHeaderName = "X-Stormpath-Date"; + void IRequestAuthenticator.Authenticate(HttpRequestMessage request, IClientApiKey apiKey) { - throw new NotImplementedException(); + var utcNow = DateTimeOffset.UtcNow; + request.Headers.Add(StormpathDateHeaderName, Iso8601.Format(utcNow)); + + var authorizationHeaderContent = $"{apiKey.GetId()}:{apiKey.GetSecret()}"; + var authorizationHeader = authorizationHeaderContent.ToBase64(Encoding.UTF8); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authorizationHeader); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs index 235be285..c3e4542e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs @@ -24,14 +24,14 @@ internal static class Iso8601 { public static string Format(DateTimeOffset dto) { - return dto.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", + return dto.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); } - public static DateTimeOffset Parse(string iso8601) + public static DateTimeOffset Parse(string iso8601String) { string pattern = "yyyy-MM-dd'T'HH:mm:ss.FFFK"; - return DateTimeOffset.ParseExact(iso8601, pattern, + return DateTimeOffset.ParseExact(iso8601String, pattern, CultureInfo.InvariantCulture); } } From b9fd12402f39824ecb24292aefdc2774826ba7c8 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 14:52:13 -0700 Subject: [PATCH 071/238] Adding and fixing tests --- .../DefaultClient_tests.cs | 31 +++++++++++ .../Integration_tests.cs | 55 +++++++++++++++++++ .../Stormpath.SDK.Tests.Integration.csproj | 2 + .../Impl/Linq/Where_tests.cs | 10 ++-- .../Impl/Utility/Iso8601_tests.cs | 8 +-- .../Impl/Http/NetHttpRequestExecutor.cs | 33 ++++++++++- 6 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests.Integration/Integration_tests.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs new file mode 100644 index 00000000..679f5e87 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using Xunit; + +namespace Stormpath.SDK.Tests.Integration +{ + public class DefaultClient_tests : Integration_tests + { + [Fact(Skip="Todo")] + public async Task Getting_current_tenant() + { + var tenant = await harness.Client.GetCurrentTenantAsync(); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Integration_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Integration_tests.cs new file mode 100644 index 00000000..5419dfc6 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Integration_tests.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Shouldly; +using Stormpath.SDK.Api; +using Stormpath.SDK.Client; + +namespace Stormpath.SDK.Tests.Integration +{ + public abstract class Integration_tests + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields must be private", Justification = "Reviewed.")] + protected readonly IntegrationHarness harness; + + public Integration_tests() + { + // Expect that API keys are in environment variables. (works with travis-ci) + var apiKey = ClientApiKeys.Builder().Build(); + apiKey.IsValid().ShouldBe(true); + + var client = Clients + .Builder() + .SetApiKey(apiKey) + .SetAuthenticationScheme(AuthenticationScheme.Basic) // TODO + .Build(); + + harness = new IntegrationHarness(client); + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "Reviewed.")] + public sealed class IntegrationHarness + { + public IntegrationHarness(IClient client) + { + this.Client = client; + } + + public IClient Client { get; } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj index a27a9e79..cda120da 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj @@ -60,6 +60,8 @@ + + diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Where_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Where_tests.cs index 92a7edfb..e8167f7d 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Where_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Where_tests.cs @@ -158,7 +158,7 @@ public void Where_date_attribute_greater_than() var query = Harness.Queryable .Where(x => x.CreatedAt > testDate); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=(2015-01-01T06:00:00.000Z,]"); + query.GeneratedArgumentsWere(Url, Resource, "createdAt=(2015-01-01T06:00:00Z,]"); } [Fact] @@ -168,7 +168,7 @@ public void Where_date_attribute_greater_than_or_equalto() var query = Harness.Queryable .Where(x => x.CreatedAt >= testDate); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=[2015-01-01T06:00:00.000Z,]"); + query.GeneratedArgumentsWere(Url, Resource, "createdAt=[2015-01-01T06:00:00Z,]"); } [Fact] @@ -178,7 +178,7 @@ public void Where_date_attribute_less_than() var query = Harness.Queryable .Where(x => x.ModifiedAt < testDate); - query.GeneratedArgumentsWere(Url, Resource, "modifiedAt=[,2016-01-01T12:00:00.000Z)"); + query.GeneratedArgumentsWere(Url, Resource, "modifiedAt=[,2016-01-01T12:00:00Z)"); } [Fact] @@ -188,7 +188,7 @@ public void Where_date_attribute_less_than_or_equalto() var query = Harness.Queryable .Where(x => x.ModifiedAt <= testDate); - query.GeneratedArgumentsWere(Url, Resource, "modifiedAt=[,2016-01-01T12:00:00.000Z]"); + query.GeneratedArgumentsWere(Url, Resource, "modifiedAt=[,2016-01-01T12:00:00Z]"); } [Fact] @@ -199,7 +199,7 @@ public void Where_date_attribute_between() var query = Harness.Queryable .Where(x => x.CreatedAt > testStartDate && x.CreatedAt <= testEndDate); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=(2015-01-01T00:00:00.000Z,2015-12-31T23:59:59.000Z]"); + query.GeneratedArgumentsWere(Url, Resource, "createdAt=(2015-01-01T00:00:00Z,2015-12-31T23:59:59Z]"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601_tests.cs index 18b8ce45..e2291d89 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601_tests.cs @@ -35,12 +35,12 @@ public void Formatting_from_local_date() // Midnight Pacific Time, Jan 1, 2015 = 2015-01-01 08:00 UTC var midnightPacificTimeJan1 = new DateTimeOffset(2015, 01, 01, 00, 00, 00, 00, TimeSpan.FromHours(-8)); var eightHoursLaterUtc8601 = Iso8601.Format(midnightPacificTimeJan1); - eightHoursLaterUtc8601.ShouldBe("2015-01-01T08:00:00.000Z"); + eightHoursLaterUtc8601.ShouldBe("2015-01-01T08:00:00Z"); // 4PM Pacific Time, Dec 31 2015 = 2015-01-01 00:00 UTC var fourInTheLocalAfternoonDec31 = new DateTime(2014, 12, 31, 16, 00, 00); var midnightUtcIso8601 = Iso8601.Format(new DateTimeOffset(fourInTheLocalAfternoonDec31, TimeZoneInfo.FindSystemTimeZoneById(pacificTimeZoneName).BaseUtcOffset)); - midnightUtcIso8601.ShouldBe("2015-01-01T00:00:00.000Z"); + midnightUtcIso8601.ShouldBe("2015-01-01T00:00:00Z"); } [Fact] @@ -48,11 +48,11 @@ public void Formatting_from_UTC_date() { var alreadyInUtc = new DateTimeOffset(2016, 02, 01, 12, 00, 00, TimeSpan.Zero); var directlyInto8601 = Iso8601.Format(alreadyInUtc); - directlyInto8601.ShouldBe("2016-02-01T12:00:00.000Z"); + directlyInto8601.ShouldBe("2016-02-01T12:00:00Z"); var dateTimeInUtc = new DateTime(2016, 02, 02, 16, 30, 00, DateTimeKind.Utc); var alsoConvertsFine = Iso8601.Format(new DateTimeOffset(dateTimeInUtc, TimeSpan.Zero)); - alsoConvertsFine.ShouldBe("2016-02-02T16:30:00.000Z"); + alsoConvertsFine.ShouldBe("2016-02-02T16:30:00Z"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs index 498f5140..d4352996 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs @@ -21,6 +21,7 @@ using System.Threading.Tasks; using Stormpath.SDK.Api; using Stormpath.SDK.Client; +using Stormpath.SDK.Impl.Http.Authentication; namespace Stormpath.SDK.Impl.Http { @@ -30,16 +31,24 @@ internal sealed class NetHttpRequestExecutor : IRequestExecutor private readonly IClientApiKey apiKey; private readonly AuthenticationScheme authScheme; private readonly int connectionTimeout; + + private readonly IRequestAuthenticator requestAuthenticator; private readonly HttpClient client; private bool disposed = false; // To detect redundant calls public NetHttpRequestExecutor(IClientApiKey apiKey, AuthenticationScheme authenticationScheme, int connectionTimeout) { + if (!apiKey.IsValid()) + throw new ApplicationException("API Key is invalid."); + this.apiKey = apiKey; this.authScheme = authenticationScheme; this.connectionTimeout = connectionTimeout; + IRequestAuthenticatorFactory requestAuthenticatorFactory = new DefaultRequestAuthenticatorFactory(); + requestAuthenticator = requestAuthenticatorFactory.Create(authenticationScheme); + client = new HttpClient(); SetupClient(); } @@ -53,9 +62,29 @@ private void SetupClient() client.Timeout = TimeSpan.FromMilliseconds(connectionTimeout); } - Task IRequestExecutor.GetAsync(string href, CancellationToken cancellationToken) + async Task IRequestExecutor.GetAsync(string href, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var request = new HttpRequestMessage(HttpMethod.Get, href); + + requestAuthenticator.Authenticate(request, apiKey); + + try + { + var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + + // TODO more refined error handling + response.EnsureSuccessStatusCode(); // throws + return null; // not reached + } + catch (Exception e) + { + throw new RequestException("Unable to execute HTTP request.", e); + } } string IRequestExecutor.GetSync(string href) From 0dcb9f9eb60edf171f984dd576fcfd52cac90113 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 21:58:22 -0700 Subject: [PATCH 072/238] UserAgent reporting --- .../DefaultClient_tests.cs | 6 +- .../Properties/AssemblyInfo.cs | 12 +- .../Impl/InternalDataStore_tests.cs | 5 +- .../Impl/Linq/ToListAsync_tests.cs | 3 +- .../Impl/Linq/Unsupported_filters_tests.cs | 9 +- .../UserAgentBuilder_tests.cs} | 22 ++- .../Properties/AssemblyInfo.cs | 12 +- .../Stormpath.SDK.Tests.csproj | 2 +- .../Stormpath.SDK/Impl/Api/ClientApiKey.cs | 2 +- .../Impl/DataStore/InternalDataStore.cs | 9 +- .../Impl/Http/IRequestExecutor.cs | 4 +- .../Impl/Http/NetHttpRequestExecutor.cs | 46 +++-- .../Impl/Http/UserAgentBuilder.cs | 82 ++++++++ .../CollectionResourceQueryModelVisitor.cs | 8 +- ...Comparer.cs => GenericEqualityComparer.cs} | 6 +- .../Stormpath.SDK/Impl/Utility/Iso8601.cs | 7 +- .../Impl/Utility/PlatformHelper.cs | 182 ++++++++++++++++++ .../Impl/Utility/SafeNativeMethods.cs | 56 ++++++ .../Impl/Utility/WindowsVersion.cs | 74 +++++++ .../Impl/Utility/WindowsVersionHelper.cs | 79 ++++++++ .../Stormpath.SDK/Properties/AssemblyInfo.cs | 14 +- .../Stormpath.SDK/Stormpath.SDK.csproj | 7 +- 22 files changed, 584 insertions(+), 63 deletions(-) rename Stormpath.SDK/Stormpath.SDK.Tests/{Helpers/PlatformHelper.cs => Impl/UserAgentBuilder_tests.cs} (58%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Http/UserAgentBuilder.cs rename Stormpath.SDK/Stormpath.SDK/Impl/Utility/{GenericComparer.cs => GenericEqualityComparer.cs} (87%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/PlatformHelper.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/SafeNativeMethods.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersion.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersionHelper.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs index 679f5e87..ab73f60e 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs @@ -15,6 +15,10 @@ // limitations under the License. // +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -22,7 +26,7 @@ namespace Stormpath.SDK.Tests.Integration { public class DefaultClient_tests : Integration_tests { - [Fact(Skip="Todo")] + [Fact(Skip ="Todo")] public async Task Getting_current_tenant() { var tenant = await harness.Client.GetCurrentTenantAsync(); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Properties/AssemblyInfo.cs index 9f299278..4f41ecc5 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Properties/AssemblyInfo.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Properties/AssemblyInfo.cs @@ -22,11 +22,11 @@ // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Stormpath.SDK.Tests.Integration")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyDescription("Integration tests for Stormpath C# SDK")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Stormpath.SDK.Tests.Integration")] -[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyCompany("Stormpath, Inc.")] +[assembly: AssemblyProduct("Stormpath C# SDK")] +[assembly: AssemblyCopyright("Copyright © 2015 Stormpath, Inc.")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -48,5 +48,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("0.2.0.0")] +[assembly: AssemblyFileVersion("0.2.0.0")] diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs index 849f5ed4..f2d82a68 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System; using System.Threading.Tasks; using NSubstitute; using Shouldly; @@ -45,7 +46,7 @@ public InternalDataStore_tests() public async Task Instantiating_Account_from_JSON() { var href = "http://foobar/account"; - fakeRequestExecutor.GetAsync(href) + fakeRequestExecutor.GetAsync(new Uri(href)) .Returns(Task.FromResult(FakeJson.Account)); var account = await dataStore.GetResourceAsync(href); @@ -79,7 +80,7 @@ public async Task Instantiating_Account_from_JSON() public async Task Instantiating_Tenant_from_JSON() { var href = "http://foobar/tenant"; - fakeRequestExecutor.GetAsync(href) + fakeRequestExecutor.GetAsync(new Uri(href)) .Returns(Task.FromResult(FakeJson.Tenant)); var tenant = await dataStore.GetResourceAsync(href); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ToListAsync_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ToListAsync_tests.cs index f34a2e44..0d1fcd27 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ToListAsync_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ToListAsync_tests.cs @@ -142,7 +142,8 @@ public async Task ForEachAsync_can_be_cancelled() try { - await harness.Queryable.ForEachAsync((acct, index) => + await harness.Queryable.ForEachAsync( + (acct, index) => { reachedIndex = index; diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Unsupported_filters_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Unsupported_filters_tests.cs index 70a45ccd..b71879de 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Unsupported_filters_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Unsupported_filters_tests.cs @@ -118,7 +118,8 @@ public void GroupJoin_clause_is_unsupported() { Should.Throw(() => { - var query = Harness.Queryable.GroupJoin(Enumerable.Empty(), + var query = Harness.Queryable.GroupJoin( + Enumerable.Empty(), outer => outer.Email, inner => inner.Username, (outer, results) => new { outer.CreatedAt, results }); @@ -141,7 +142,8 @@ public void Join_clause_is_unsupported() { Should.Throw(() => { - var query = Harness.Queryable.Join(Enumerable.Empty(), + var query = Harness.Queryable.Join( + Enumerable.Empty(), outer => outer.Email, inner => inner.Username, (outer, inner) => outer.CreatedAt); @@ -275,7 +277,8 @@ public void Zip_is_unsupported() { Should.Throw(() => { - var query = Harness.Queryable.Zip(Enumerable.Empty(), + var query = Harness.Queryable.Zip( + Enumerable.Empty(), (first, second) => first.Email == second.Email); query.GeneratedArgumentsWere(Url, Resource, ""); }); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/PlatformHelper.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/UserAgentBuilder_tests.cs similarity index 58% rename from Stormpath.SDK/Stormpath.SDK.Tests/Helpers/PlatformHelper.cs rename to Stormpath.SDK/Stormpath.SDK.Tests/Impl/UserAgentBuilder_tests.cs index 1a4287c2..f577f786 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/PlatformHelper.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/UserAgentBuilder_tests.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -16,19 +16,23 @@ // using System; +using Shouldly; +using Stormpath.SDK.Impl.Http; +using Xunit; -namespace Stormpath.SDK.Tests.Helpers +namespace Stormpath.SDK.Tests.Impl { - public static class PlatformHelper + public class UserAgentBuilder_tests { - private static readonly Lazy IsRunningOnMonoValue = new Lazy(() => + [Fact] + public void UserAgent_is_known() { - return Type.GetType("Mono.Runtime") != null; - }); + var userAgent = UserAgentBuilder.GetUserAgent(); - public static bool IsRunningOnMono() - { - return IsRunningOnMonoValue.Value; + userAgent.ShouldNotContain("unknown"); + + // TODO change this when moving to xUnit 2.0+ + Console.WriteLine($"UserAgent: {userAgent}"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Properties/AssemblyInfo.cs index 5edff45e..c41d96d0 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Properties/AssemblyInfo.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Properties/AssemblyInfo.cs @@ -23,11 +23,11 @@ // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Stormpath.SDK.Tests")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyDescription("Unit tests for Stormpath C# SDK")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Stormpath.SDK.Tests")] -[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyCompany("Stormpath, Inc.")] +[assembly: AssemblyProduct("Stormpath C# SDK")] +[assembly: AssemblyCopyright("Copyright © 2015 Stormpath, Inc.")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -49,5 +49,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("0.2.0.0")] +[assembly: AssemblyFileVersion("0.2.0.0")] diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 37e4d39a..a1e0b1de 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -78,7 +78,6 @@ - @@ -103,6 +102,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKey.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKey.cs index 0faa27a9..58a9caf6 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKey.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Api/ClientApiKey.cs @@ -67,7 +67,7 @@ bool IClientApiKey.IsValid() /// internal partial class ClientApiKey : IEquatable { - private static readonly GenericComparer Comparer = new GenericComparer( + private static readonly GenericEqualityComparer Comparer = new GenericEqualityComparer( (ClientApiKey x, ClientApiKey y) => { return x.id == y.id && x.secret == y.secret; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs index dd2a0541..42b24436 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs @@ -49,7 +49,14 @@ internal InternalDataStore(IRequestExecutor requestExecutor, string baseUrl) async Task IDataStore.GetResourceAsync(string href, CancellationToken cancellationToken) { - var json = await requestExecutor.GetAsync(href, cancellationToken); + if (!href.StartsWith("https://")) + href = $"{baseUrl}/{href}"; + + Uri hrefUri; + if (!Uri.TryCreate(href, UriKind.Absolute, out hrefUri)) + throw new RequestException($"The URI is not valid: {href}"); + + var json = await requestExecutor.GetAsync(hrefUri, cancellationToken); var map = mapMarshaller.Deserialize(json); var resource = resourceFactory.Instantiate(map); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/IRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/IRequestExecutor.cs index 725a1dd4..c576881e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/IRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/IRequestExecutor.cs @@ -28,8 +28,8 @@ internal interface IRequestExecutor : IDisposable int ConnectionTimeout { get; } - string GetSync(string href); + string GetSync(Uri href); - Task GetAsync(string href, CancellationToken cancellationToken = default(CancellationToken)); + Task GetAsync(Uri href, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs index d4352996..0e67ace7 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs @@ -16,7 +16,9 @@ // using System; +using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Stormpath.SDK.Api; @@ -60,38 +62,54 @@ public NetHttpRequestExecutor(IClientApiKey apiKey, AuthenticationScheme authent private void SetupClient() { client.Timeout = TimeSpan.FromMilliseconds(connectionTimeout); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(UserAgentBuilder.GetUserAgent())); } - async Task IRequestExecutor.GetAsync(string href, CancellationToken cancellationToken) + async Task IRequestExecutor.GetAsync(Uri href, CancellationToken cancellationToken) { - var request = new HttpRequestMessage(HttpMethod.Get, href); + Uri currentUri = href; - requestAuthenticator.Authenticate(request, apiKey); - - try + while (true) { + var request = new HttpRequestMessage(HttpMethod.Get, href); + requestAuthenticator.Authenticate(request, apiKey); var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); + if (IsRedirect(response)) + { + currentUri = response.Headers.Location; + continue; + } + if (response.IsSuccessStatusCode) { return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } - - // TODO more refined error handling - response.EnsureSuccessStatusCode(); // throws - return null; // not reached - } - catch (Exception e) - { - throw new RequestException("Unable to execute HTTP request.", e); + else + { + var content = await response.Content.ReadAsStringAsync(); + throw new RequestException($"Unable to execute HTTP request. Message: '{content}'"); + } } } - string IRequestExecutor.GetSync(string href) + string IRequestExecutor.GetSync(Uri href) { throw new NotImplementedException(); } + private bool IsRedirect(HttpResponseMessage response) + { + bool moved = + response.StatusCode == HttpStatusCode.MovedPermanently || // 301 + response.StatusCode == HttpStatusCode.Redirect || // 302 + response.StatusCode == HttpStatusCode.TemporaryRedirect; // 307 + bool hasNewLocation = !string.IsNullOrEmpty(response.Headers.Location?.AbsolutePath); + + return moved && hasNewLocation; + } + #region IDisposable implementation private void Dispose(bool disposing) diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/UserAgentBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/UserAgentBuilder.cs new file mode 100644 index 00000000..66c1016e --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/UserAgentBuilder.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using Stormpath.SDK.Impl.Utility; + +namespace Stormpath.SDK.Impl.Http +{ + internal static class UserAgentBuilder + { + // Lazy ensures this only runs once and is cached. + private static readonly Lazy SdkUserAgentValue = new Lazy(() => GetSdkUserAgentImpl()); + + private static string GetSdkUserAgentImpl() + { + return string.Join( + " ", + string.Join(" ", GetInstalledIntegrationsInfo()), + GetSdkInfo(), + GetRuntimeInfo(), + GetPlatformInfo()) + .Trim(); + } + + public static string GetUserAgent() + { + return SdkUserAgentValue.Value; + } + + private static IEnumerable GetInstalledIntegrationsInfo() + { + // TODO + return Enumerable.Empty(); + } + + private static string GetSdkInfo() + { + var version = typeof(UserAgentBuilder).Assembly.GetName() + .Version.ToString() + .Replace(".0", string.Empty); // remove unnecessary .0.0 + + return $"stormpath-sdk-csharp/{version}"; + } + + private static string GetRuntimeInfo() + { + string runtimeInfo; + + if (PlatformHelper.IsRunningOnMono()) + { + runtimeInfo = $"mono/{PlatformHelper.GetMonoRuntimeVersion()} mono-dotnetframework/{PlatformHelper.GetMonoDotNetFrameworkVersion()}"; + } + else + { + runtimeInfo = $"dotnetframework/{PlatformHelper.GetDotNetFrameworkVersion()}"; + } + + return runtimeInfo; + } + + private static string GetPlatformInfo() + { + return $"{PlatformHelper.GetOSName()}/{PlatformHelper.GetOSVersion()}"; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs index 3eb92091..8d0d9743 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryModelVisitor.cs @@ -167,9 +167,11 @@ private bool HandleExpandExtensionResultOperator(ResultOperatorBase resultOperat if (CollectionLinkMethodNameTranslator.TryGetValue(methodCallExpression.Method.Name, out expandField)) { - ParsedModel.Expansions.Add(new ExpansionTerm(expandField, - (int?)expandResultOperator.Offset.Value, - (int?)expandResultOperator.Limit.Value)); + ParsedModel.Expansions.Add( + new ExpansionTerm( + expandField, + (int?)expandResultOperator.Offset.Value, + (int?)expandResultOperator.Limit.Value)); return true; // done } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericEqualityComparer.cs similarity index 87% rename from Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericEqualityComparer.cs index 4a293f1f..4e75c565 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericComparer.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/GenericEqualityComparer.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -20,12 +20,12 @@ namespace Stormpath.SDK.Impl.Utility { - internal sealed class GenericComparer : IEqualityComparer + internal sealed class GenericEqualityComparer : IEqualityComparer { private readonly Func areEqualFunc; private readonly Func hashFunc; - public GenericComparer(Func areEqualFunc, Func hashFunc) + public GenericEqualityComparer(Func areEqualFunc, Func hashFunc) { this.areEqualFunc = areEqualFunc; this.hashFunc = hashFunc; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs index c3e4542e..bd2ce610 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs @@ -24,14 +24,17 @@ internal static class Iso8601 { public static string Format(DateTimeOffset dto) { - return dto.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ", + return dto.UtcDateTime.ToString( + "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); } public static DateTimeOffset Parse(string iso8601String) { string pattern = "yyyy-MM-dd'T'HH:mm:ss.FFFK"; - return DateTimeOffset.ParseExact(iso8601String, pattern, + return DateTimeOffset.ParseExact( + iso8601String, + pattern, CultureInfo.InvariantCulture); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/PlatformHelper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/PlatformHelper.cs new file mode 100644 index 00000000..a9e280b8 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/PlatformHelper.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Stormpath.SDK.Impl.Utility +{ + public static class PlatformHelper + { + // Lazy will cache this result so the semi-expensive reflection call only happens once. + private static readonly Lazy IsRunningOnMonoValue = new Lazy(() => + { + return Type.GetType("Mono.Runtime") != null; + }); + + public static bool IsRunningOnMono() + { + return IsRunningOnMonoValue.Value; + } + + public static string GetMonoRuntimeVersion() + { + if (!IsRunningOnMono()) + return "unknown-1"; + + var monoRuntimeType = Type.GetType("Mono.Runtime"); + if (monoRuntimeType == null) + return "unknown-2"; + + var displayNameMethod = monoRuntimeType.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); + if (displayNameMethod == null) + return "unknown-3"; + + var fullVersionString = displayNameMethod.Invoke(null, null)?.ToString(); + if (string.IsNullOrEmpty(fullVersionString)) + return "unknown-4"; + + if (!fullVersionString.Contains(" ")) + return fullVersionString; + + var version = fullVersionString.Split(' ')?[0]; + if (string.IsNullOrEmpty(version)) + return fullVersionString; + + return version; + } + + public static string GetMonoDotNetFrameworkVersion() + { + if (!IsRunningOnMono()) + return "unknown"; + + var version = $"{Environment.Version.Major}.{Environment.Version}"; + if (Environment.Version.MajorRevision > 0) + version = $"{version}.{Environment.Version.MajorRevision}"; + + return version; + } + + public static string GetDotNetFrameworkVersion() + { + var olderFrameworkVersion = GetDotNet1Thru4Version(); + var newerFrameworkVersion = GetDotNet45OrNewerVersion(); + + if (!string.IsNullOrEmpty(newerFrameworkVersion)) + return newerFrameworkVersion; + + if (!string.IsNullOrEmpty(olderFrameworkVersion)) + return olderFrameworkVersion; + + return "unknown"; + } + + private static string GetDotNet1Thru4Version() + { + // Returns empty string if no results are found + try + { + var installedVersions = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP"); + var installedVersionNames = installedVersions?.GetSubKeyNames(); + var latestVersion = installedVersionNames?.LastOrDefault(); + if (string.IsNullOrEmpty(latestVersion)) + return string.Empty; + + var frameworkVersion = latestVersion.Remove(0, 1); // trim leading 'v' + + int servicePackVersion = 0; + bool servicePackExists = int.TryParse(installedVersions.OpenSubKey(latestVersion)?.GetValue("SP", 0)?.ToString(), out servicePackVersion); + if (!servicePackExists || servicePackVersion == 0) + return frameworkVersion; + + return $"{frameworkVersion}-SP{servicePackVersion}"; + } + catch (Exception) + { + return string.Empty; + } + } + + private static string GetDotNet45OrNewerVersion() + { + // Returns empty string if no results are found + try + { + using (var ndpKey = Microsoft.Win32.RegistryKey + .OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryView.Registry32) + .OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full\\")) + { + int releaseKey; + if (!int.TryParse(ndpKey?.GetValue("Release")?.ToString(), out releaseKey)) + return string.Empty; + + if (releaseKey >= 393273) + return "4.6RC+"; + if (releaseKey >= 379893) + return "4.5.2"; + if (releaseKey >= 378675) + return "4.5.1"; + if (releaseKey >= 378389) + return "4.5"; + + return string.Empty; + } + } + catch (Exception) + { + return string.Empty; + } + } + + public static string GetOSName() + { + int platformID = (int)Environment.OSVersion.Platform; + + switch (platformID) + { + case (int)PlatformID.MacOSX: + case (int)PlatformID.Unix: + case 128: + return "Unix"; + case (int)PlatformID.Win32NT: + case (int)PlatformID.Win32S: + case (int)PlatformID.Win32Windows: + case (int)PlatformID.WinCE: + return "Windows"; + case (int)PlatformID.Xbox: + return "Xbox"; + default: + return $"UnkownOS(PlatformID-{platformID})"; + } + } + + public static string GetOSVersion() + { + return IsRunningOnMono() + ? GetMonoOSVersion() + : WindowsVersionHelper.GetWindowsOSVersion(); + } + + private static string GetMonoOSVersion() + { + return Environment.OSVersion.VersionString.Replace(' ', '-'); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/SafeNativeMethods.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/SafeNativeMethods.cs new file mode 100644 index 00000000..f9377edb --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/SafeNativeMethods.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Runtime.InteropServices; +using System.Security; + +namespace Stormpath.SDK.Impl.Utility +{ + [SuppressUnmanagedCodeSecurityAttribute] + internal static class SafeNativeMethods + { + [DllImport("kernel32.dll")] + internal static extern bool GetVersionEx(ref OSVERSIONINFOEX osVersionInfo); + + [StructLayout(LayoutKind.Sequential)] + internal struct OSVERSIONINFOEX + { + public int dwOSVersionInfoSize; + + public int dwMajorVersion; + + public int dwMinorVersion; + + public int dwBuildNumber; + + public int dwPlatformId; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szCSDVersion; + + public short wServicePackMajor; + + public short wServicePackMinor; + + public short wSuiteMask; + + public byte wProductType; + + public byte wReserved; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersion.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersion.cs new file mode 100644 index 00000000..9f134060 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersion.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Stormpath.SDK.Impl.Utility +{ + internal partial struct WindowsVersion + { + private readonly int major; + private readonly int minor; + private readonly int? productType; + + public WindowsVersion(int major, int minor, int? productType = null) + { + this.major = major; + this.minor = minor; + this.productType = productType; + } + } + + internal partial struct WindowsVersion : IEquatable + { + private static readonly GenericEqualityComparer Comparer = new GenericEqualityComparer( + (WindowsVersion x, WindowsVersion y) => + { + bool majorMinorMatch = x.major == y.major && x.minor == y.minor; + bool productTypeMatchesIfExists = x.productType.HasValue + ? x.productType == y.productType + : true; + + return majorMinorMatch && productTypeMatchesIfExists; + }, + (WindowsVersion wv) => + { + return HashCode.Start + .Hash(wv.major) + .Hash(wv.minor) + .Hash(wv.productType); + }); + + public bool Equals(WindowsVersion other) + { + return Comparer.Equals(this, other); + } + + public override bool Equals(object obj) + { + if (!(obj is WindowsVersion)) + return false; + + return Comparer.Equals(this, (WindowsVersion)obj); + } + + public override int GetHashCode() + { + return Comparer.GetHashCode(this); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersionHelper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersionHelper.cs new file mode 100644 index 00000000..c2f61953 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersionHelper.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Utility +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Reviewed.")] + public static class WindowsVersionHelper + { + // List from https://msdn.microsoft.com/en-us/library/windows/desktop/ms724833(v=vs.85).aspx + private static readonly int NTWorkstation = 1; + private static readonly int NTDomainController = 2; + private static readonly int NTServer = 3; + private static readonly Dictionary WindowsVersionLookupTable = + new Dictionary() + { + { new WindowsVersion(5, 0), "2000" }, + { new WindowsVersion(5, 1), "XP" }, + { new WindowsVersion(5, 2), "Server-2003" }, + { new WindowsVersion(6, 0, NTWorkstation), "Vista" }, + { new WindowsVersion(6, 0, NTDomainController), "Server-2008" }, + { new WindowsVersion(6, 0, NTServer), "Server-2008" }, + { new WindowsVersion(6, 1, NTWorkstation), "7" }, + { new WindowsVersion(6, 1, NTDomainController), "Server-2008-R2" }, + { new WindowsVersion(6, 1, NTServer), "Server-2008-R2" }, + { new WindowsVersion(6, 2, NTWorkstation), "8" }, + { new WindowsVersion(6, 2, NTDomainController), "Server-2012" }, + { new WindowsVersion(6, 2, NTServer), "Server-2012" }, + { new WindowsVersion(6, 3, NTWorkstation), "8.1" }, + { new WindowsVersion(6, 3, NTDomainController), "Server-2012-R2" }, + { new WindowsVersion(6, 3, NTServer), "Server-2012-R2" }, + { new WindowsVersion(10, 0), "10" }, + }; + + public static string GetWindowsOSVersion() + { + var major = Environment.OSVersion.Version.Major; + var minor = Environment.OSVersion.Version.Minor; + var productType = GetProductType(); + + string version; + if (!WindowsVersionLookupTable.TryGetValue(new WindowsVersion(major, minor, productType), out version)) + return $"unknown-{major}.{minor}.{productType}"; + + return version; + } + + private static int? GetProductType() + { + var osVersionInfo = new SafeNativeMethods.OSVERSIONINFOEX(); + osVersionInfo.dwOSVersionInfoSize = Marshal.SizeOf(typeof(SafeNativeMethods.OSVERSIONINFOEX)); + + if (!SafeNativeMethods.GetVersionEx(ref osVersionInfo)) + return null; + + return osVersionInfo.wProductType; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs index 5c360099..603a4375 100644 --- a/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs +++ b/Stormpath.SDK/Stormpath.SDK/Properties/AssemblyInfo.cs @@ -22,12 +22,12 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("Stormpath.Core")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyTitle("Stormpath.SDK")] +[assembly: AssemblyDescription("The Official Stormpath SDK for .NET")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Stormpath.Core")] -[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyCompany("Stormpath, Inc.")] +[assembly: AssemblyProduct("Stormpath C# SDK")] +[assembly: AssemblyCopyright("Copyright © 2015 Stormpath, Inc.")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -49,7 +49,7 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("0.2.0.0")] +[assembly: AssemblyFileVersion("0.2.0.0")] [assembly: InternalsVisibleTo("Stormpath.SDK.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index dfda7e37..b268dbad 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -87,6 +87,7 @@ + @@ -141,6 +142,10 @@ + + + + @@ -157,7 +162,7 @@ - + From a33709a4d64d650cd74bc958e9f65446eae15d68 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 22:17:18 -0700 Subject: [PATCH 073/238] Test fixes --- .../Impl/DefaultClientApiKeyBuilder_tests.cs | 14 ++++++++++++-- .../Impl/InternalDataStore_tests.cs | 3 ++- .../Impl/DataStore/InternalDataStore.cs | 6 +++--- .../Impl/Http/NetHttpRequestExecutor.cs | 3 ++- .../Stormpath.SDK/Impl/Utility/PlatformHelper.cs | 13 ++++++++----- .../Impl/Utility/WindowsVersionHelper.cs | 2 +- 6 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientApiKeyBuilder_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientApiKeyBuilder_tests.cs index a92449e3..9e87c222 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientApiKeyBuilder_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientApiKeyBuilder_tests.cs @@ -29,12 +29,22 @@ public class DefaultClientApiKeyBuilder_tests { public class With_missing_values { + private IClientApiKeyBuilder builder; + + public With_missing_values() + { + this.builder = new DefaultClientApiKeyBuilder( + Substitute.For(), + Substitute.For(), + Substitute.For()); + } + [Fact] public void With_missing_id_throws_error() { Assert.Throws(() => { - var clientApiKey = ClientApiKeys.Builder() + var clientApiKey = builder .SetSecret("foo") .Build(); }); @@ -45,7 +55,7 @@ public void With_missing_secret_throws_error() { Assert.Throws(() => { - var clientApiKey = ClientApiKeys.Builder() + var clientApiKey = builder .SetId("foo") .Build(); }); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs index f2d82a68..5d3c9a14 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs @@ -16,6 +16,7 @@ // using System; +using System.Threading; using System.Threading.Tasks; using NSubstitute; using Shouldly; @@ -46,7 +47,7 @@ public InternalDataStore_tests() public async Task Instantiating_Account_from_JSON() { var href = "http://foobar/account"; - fakeRequestExecutor.GetAsync(new Uri(href)) + fakeRequestExecutor.GetAsync(new Uri(href), Arg.Any()) .Returns(Task.FromResult(FakeJson.Account)); var account = await dataStore.GetResourceAsync(href); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs index 42b24436..ae4c6aba 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs @@ -49,11 +49,11 @@ internal InternalDataStore(IRequestExecutor requestExecutor, string baseUrl) async Task IDataStore.GetResourceAsync(string href, CancellationToken cancellationToken) { - if (!href.StartsWith("https://")) + if (!href.StartsWith("http")) href = $"{baseUrl}/{href}"; - Uri hrefUri; - if (!Uri.TryCreate(href, UriKind.Absolute, out hrefUri)) + var hrefUri = new Uri(href); + if (!hrefUri.IsWellFormedOriginalString()) throw new RequestException($"The URI is not valid: {href}"); var json = await requestExecutor.GetAsync(hrefUri, cancellationToken); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs index 0e67ace7..371c36bb 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs @@ -63,7 +63,8 @@ private void SetupClient() { client.Timeout = TimeSpan.FromMilliseconds(connectionTimeout); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(UserAgentBuilder.GetUserAgent())); + // client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(UserAgentBuilder.GetUserAgent())); + client.DefaultRequestHeaders.Add("UserAgent", UserAgentBuilder.GetUserAgent()); } async Task IRequestExecutor.GetAsync(Uri href, CancellationToken cancellationToken) diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/PlatformHelper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/PlatformHelper.cs index a9e280b8..113c0b7c 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/PlatformHelper.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/PlatformHelper.cs @@ -22,7 +22,7 @@ namespace Stormpath.SDK.Impl.Utility { - public static class PlatformHelper + internal static class PlatformHelper { // Lazy will cache this result so the semi-expensive reflection call only happens once. private static readonly Lazy IsRunningOnMonoValue = new Lazy(() => @@ -67,9 +67,7 @@ public static string GetMonoDotNetFrameworkVersion() if (!IsRunningOnMono()) return "unknown"; - var version = $"{Environment.Version.Major}.{Environment.Version}"; - if (Environment.Version.MajorRevision > 0) - version = $"{version}.{Environment.Version.MajorRevision}"; + var version = $"{Environment.Version.Major}.{Environment.Version.Minor}"; return version; } @@ -176,7 +174,12 @@ public static string GetOSVersion() private static string GetMonoOSVersion() { - return Environment.OSVersion.VersionString.Replace(' ', '-'); + var osVersion = Environment.OSVersion.Version; + var version = $"{osVersion.Major}.{osVersion.Minor}.{osVersion.MajorRevision}.{osVersion.MinorRevision}"; + if (!string.IsNullOrEmpty(Environment.OSVersion.ServicePack)) + version = $"{version}-{Environment.OSVersion.ServicePack}"; + + return version; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersionHelper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersionHelper.cs index c2f61953..bd253d98 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersionHelper.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/WindowsVersionHelper.cs @@ -25,7 +25,7 @@ namespace Stormpath.SDK.Impl.Utility { [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Reviewed.")] - public static class WindowsVersionHelper + internal static class WindowsVersionHelper { // List from https://msdn.microsoft.com/en-us/library/windows/desktop/ms724833(v=vs.85).aspx private static readonly int NTWorkstation = 1; From ae4fc73852e72b3045a560c30800c8231d05953f Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 22:38:09 -0700 Subject: [PATCH 074/238] First working integration test! It's aliiiive! --- .../DefaultClient_tests.cs | 11 +++++------ .../Impl/Http/NetHttpRequestExecutor.cs | 15 +++++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs index ab73f60e..eba524cd 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs @@ -15,21 +15,20 @@ // limitations under the License. // -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; +using Shouldly; using Xunit; namespace Stormpath.SDK.Tests.Integration { public class DefaultClient_tests : Integration_tests { - [Fact(Skip ="Todo")] + [Fact] public async Task Getting_current_tenant() { var tenant = await harness.Client.GetCurrentTenantAsync(); + + // TODO Verify tenant data? } } -} +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs index 371c36bb..4c993db8 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs @@ -35,7 +35,7 @@ internal sealed class NetHttpRequestExecutor : IRequestExecutor private readonly int connectionTimeout; private readonly IRequestAuthenticator requestAuthenticator; - private readonly HttpClient client; + private HttpClient client; private bool disposed = false; // To detect redundant calls @@ -51,7 +51,6 @@ public NetHttpRequestExecutor(IClientApiKey apiKey, AuthenticationScheme authent IRequestAuthenticatorFactory requestAuthenticatorFactory = new DefaultRequestAuthenticatorFactory(); requestAuthenticator = requestAuthenticatorFactory.Create(authenticationScheme); - client = new HttpClient(); SetupClient(); } @@ -61,10 +60,18 @@ public NetHttpRequestExecutor(IClientApiKey apiKey, AuthenticationScheme authent private void SetupClient() { + var clientSettings = new HttpClientHandler() + { + AllowAutoRedirect = false, + + // Proxy... + }; + + client = new HttpClient(clientSettings); client.Timeout = TimeSpan.FromMilliseconds(connectionTimeout); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(UserAgentBuilder.GetUserAgent())); - client.DefaultRequestHeaders.Add("UserAgent", UserAgentBuilder.GetUserAgent()); + client.DefaultRequestHeaders.Add("User-Agent", UserAgentBuilder.GetUserAgent()); } async Task IRequestExecutor.GetAsync(Uri href, CancellationToken cancellationToken) @@ -73,7 +80,7 @@ async Task IRequestExecutor.GetAsync(Uri href, CancellationToken cancell while (true) { - var request = new HttpRequestMessage(HttpMethod.Get, href); + var request = new HttpRequestMessage(HttpMethod.Get, currentUri); requestAuthenticator.Authenticate(request, apiKey); var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); From 12fd7b49f110ce58b7a90a2852b2a95272da8c1c Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 22:43:55 -0700 Subject: [PATCH 075/238] Added secure API key environment variables to travis --- .travis.yml | 15 ++++++++++----- .../DefaultClient_tests.cs | 6 +++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index c3a5a6d2..2055be67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,13 @@ language: csharp -solution: ./Stormpath.SDK/Stormpath.SDK.sln +solution: "./Stormpath.SDK/Stormpath.SDK.sln" install: - - nuget restore -source "https://www.nuget.org/api/v2;https://www.myget.org/F/xunit" ./Stormpath.SDK/Stormpath.SDK.sln - - nuget install xunit.runners -Version 1.9.2 -OutputDirectory testrunner +- nuget restore -source "https://www.nuget.org/api/v2;https://www.myget.org/F/xunit" + ./Stormpath.SDK/Stormpath.SDK.sln +- nuget install xunit.runners -Version 1.9.2 -OutputDirectory testrunner script: - - xbuild /p:Configuration=Release ./Stormpath.SDK/Stormpath.SDK.sln - - mono ./testrunner/xunit.runners.1.9.2/tools/xunit.console.clr4.exe ./Stormpath.SDK/Stormpath.SDK.Tests/bin/Release/Stormpath.SDK.Tests.dll +- xbuild /p:Configuration=Release ./Stormpath.SDK/Stormpath.SDK.sln +- mono ./testrunner/xunit.runners.1.9.2/tools/xunit.console.clr4.exe ./Stormpath.SDK/Stormpath.SDK.Tests/bin/Release/Stormpath.SDK.Tests.dll +env: + global: + - secure: sMEfLcgXwYvRsaVRFVDzHQoMCNwgtC39F4VQe3fdq+q4oIcOYn1+e/80ZgfP3JqSTcJyhEtNfQLU1D3rYa5mfCsZjeU9EHabK1fheMRgMY9VMR24v1SG/54wXY9teBnUm1BIi+/yFdybl8a02KsvhwWqHhFVthJQqVV4y7SnQk8BKJJ/75b2Vr/Rq6W6DSXPeZoXif+uvNbFkngE7Dy8d3nhjOybh9aXSv3oWyMKIrR5zvMCNCHsPeW2+X+3x5Cr8ryMnuQ+b3tX8qlPhxyeUvcTlFcCuZYHHs/4CTHL9zEcaAkU4bYg1zfDRDHiJ/e3h591KfVSS+E/1mh29XsnE1NMIbWHKJpNB0KlW5z4qO7t7Atgsb9FOuvvqCYFvkI9WmM08ilhTt369sEXNKsEkLTsAtYH2igWng7J6meAnj5fyRcN7d8VlPRPyUbzW8RFKQn69wF7nmfU2wYQaRfbiDpYOcHOxtOA2SNuYVG2Ysf6JMIgWzeKcRwC6HWjPZu/kNk6+wGQv2d2VSapex1/tXazB8+8cpjCGK6Kqw115Y01JZ6ePfj584Wfstr3XlVSfeu9hCvGhO5EhLBq6KjL5TdfnkaFtq2CFsSHjYHumQNwW0rgmcL6nqs2SrG/feut5GUSVqZKgy9xy/7TOxoHwrnTXZqu4TgOSzpN82oNXkg= + - secure: LU2ISkFH002BC+ZSfz/LbK6+IzZ0502SkUvytbjHh+rEt3Q509xi8zd6tzxX7SfLFAIqLOW9KM+MsVMImcJPp1QKegE1ly8CjbLxXlXIurkxvvm7XERfAZWaJP8WzMVhlLpcDyIc4fC3FkHpaAivRX0XQmVElR78HnxB/+P9lD0FsXlwjpKWH9QZUARJSuh+KCmLgpgQPF1+PBGNA5koP84Ky6oGvWGcbHOK6ZwkeaC37/l5Hmi/yOnmU0Eu5BK5N6+iBQpZwNs1iEBFijMdsai3KooodK9/7m1B99bi178w9iSvrVw0ZBVGbfYDYoDnCT6FhAKIPXjYHWhQyKZ6dguKnxlIBDq9gE4PBQSfTfT7blmeJtPJNwg5INiy72XXRNT9FaFv2usWRgZstPkAg7LkYrmzJtM1oP/zAkavMTvohjzuFUpFR9NQAClUD08wbq1WBY50VgJ8cNHYeqh5J04DRhgH5Gx7O0bPHw6PmuFjU4PZ7zmzfPUo74rdqa/KdmuMSvtx/pWeMpdSKLWgBOYrVagyOkweHk78GAOMDOT6IQHD0kK/rEpQ5qxbmjIpD3I/VKp0drszo+lUz7tlRKnd+wC2KrTYKqqQAVBkD0htmrw9WhSebrLFT8HXvkZpxq9zCV9h99CUL2/Afg3oDBnNGjkABYAHNtSRT9HHzZY= diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs index eba524cd..a07324ed 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs @@ -28,7 +28,11 @@ public async Task Getting_current_tenant() { var tenant = await harness.Client.GetCurrentTenantAsync(); - // TODO Verify tenant data? + tenant.ShouldNotBe(null); + tenant.Href.ShouldNotBe(null); + tenant.Name.ShouldNotBe(null); + + // TODO - verify actual tenant data? } } } \ No newline at end of file From 86beea16898d3b14723b7dd9fa6d0fca68c76b34 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 19 Aug 2015 22:45:56 -0700 Subject: [PATCH 076/238] Point test runner to integration tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2055be67..2efd1292 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ install: script: - xbuild /p:Configuration=Release ./Stormpath.SDK/Stormpath.SDK.sln - mono ./testrunner/xunit.runners.1.9.2/tools/xunit.console.clr4.exe ./Stormpath.SDK/Stormpath.SDK.Tests/bin/Release/Stormpath.SDK.Tests.dll +- mono ./testrunner/xunit.runners.1.9.2/tools/xunit.console.clr4.exe ./Stormpath.SDK/Stormpath.SDK.Tests.Integration/bin/Release/Stormpath.SDK.Tests.Integration.dll env: global: - secure: sMEfLcgXwYvRsaVRFVDzHQoMCNwgtC39F4VQe3fdq+q4oIcOYn1+e/80ZgfP3JqSTcJyhEtNfQLU1D3rYa5mfCsZjeU9EHabK1fheMRgMY9VMR24v1SG/54wXY9teBnUm1BIi+/yFdybl8a02KsvhwWqHhFVthJQqVV4y7SnQk8BKJJ/75b2Vr/Rq6W6DSXPeZoXif+uvNbFkngE7Dy8d3nhjOybh9aXSv3oWyMKIrR5zvMCNCHsPeW2+X+3x5Cr8ryMnuQ+b3tX8qlPhxyeUvcTlFcCuZYHHs/4CTHL9zEcaAkU4bYg1zfDRDHiJ/e3h591KfVSS+E/1mh29XsnE1NMIbWHKJpNB0KlW5z4qO7t7Atgsb9FOuvvqCYFvkI9WmM08ilhTt369sEXNKsEkLTsAtYH2igWng7J6meAnj5fyRcN7d8VlPRPyUbzW8RFKQn69wF7nmfU2wYQaRfbiDpYOcHOxtOA2SNuYVG2Ysf6JMIgWzeKcRwC6HWjPZu/kNk6+wGQv2d2VSapex1/tXazB8+8cpjCGK6Kqw115Y01JZ6ePfj584Wfstr3XlVSfeu9hCvGhO5EhLBq6KjL5TdfnkaFtq2CFsSHjYHumQNwW0rgmcL6nqs2SrG/feut5GUSVqZKgy9xy/7TOxoHwrnTXZqu4TgOSzpN82oNXkg= From 23f3b0480550971755fe99a18164e242a829efe9 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 20 Aug 2015 09:06:22 -0700 Subject: [PATCH 077/238] SAuthc1 first pass (not complete) --- .../Impl/QueryString_tests.cs | 78 ++++++++++ .../Impl/Utility/Iso8601_tests.cs | 1 - .../Impl/Utility/RequestHelper_tests.cs | 60 +++++++ .../Stormpath.SDK.Tests.csproj | 2 + .../SAuthc1RequestAuthenticator.cs | 146 ++++++++++++++++++ .../Impl/Http/NetHttpRequestExecutor.cs | 1 - .../Impl/Http/Support/QueryString.cs | 96 ++++++++++++ .../Impl/Utility/RequestHelper.cs | 45 ++++++ .../Stormpath.SDK/Stormpath.SDK.csproj | 2 + 9 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/QueryString_tests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/RequestHelper_tests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Http/Support/QueryString.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/RequestHelper.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/QueryString_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/QueryString_tests.cs new file mode 100644 index 00000000..73264262 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/QueryString_tests.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Shouldly; +using Stormpath.SDK.Impl.Http.Support; +using Xunit; + +namespace Stormpath.SDK.Tests.Impl +{ + public class QueryString_tests + { + [Fact] + public void With_no_parameters() + { + var uri = new Uri("http://foo/bar"); + var qs = new QueryString(uri); + + qs.ToString().ShouldBe(string.Empty); + qs.ToString(canonical: true).ShouldBe(string.Empty); + } + + [Fact] + public void Creating_from_string() + { + var qs = new QueryString("http://foo.bar/baz/?test1=2&foo=bar"); + + qs.ToString().ShouldBe("foo=bar&test1=2"); + } + + [Fact] + public void Keys_and_values_are_lowercase() + { + var qs = new QueryString(new Uri("http://foo.bar/baz?TEST=foo&bar=BAZ")); + + qs.ToString().ShouldBe("bar=baz&test=foo"); + } + + [Fact] + public void Keys_are_sorted() + { + var qs = new QueryString(new Uri("https://foo.bar/?zulu=5&beta=foo&alpha=9")); + + qs.ToString(canonical: true).ShouldBe("alpha=9&beta=foo&zulu=5"); + } + + [Fact] + public void Keys_with_no_values_are_ok() + { + var qs = new QueryString(new Uri("https://foo.bar?foo=bar&baz=&three")); + + qs.ToString().ShouldBe("baz=&foo=bar&three="); + } + + [Fact] + public void Canonicalize_flag_is_observed() + { + var qs = new QueryString("search=start*&test=one two&tilde=~"); + + qs.ToString().ShouldBe("search=start*&test=one+two&tilde=%7E"); + qs.ToString(canonical: true).ShouldBe("search=start%2A&test=one%20two&tilde=~"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601_tests.cs index e2291d89..9efbd6b4 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/Iso8601_tests.cs @@ -18,7 +18,6 @@ using System; using Shouldly; using Stormpath.SDK.Impl.Utility; -using Stormpath.SDK.Tests.Helpers; using Xunit; namespace Stormpath.SDK.Tests.Impl.Utility diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/RequestHelper_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/RequestHelper_tests.cs new file mode 100644 index 00000000..71bae041 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Utility/RequestHelper_tests.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Shouldly; +using Stormpath.SDK.Impl.Utility; +using Xunit; + +namespace Stormpath.SDK.Tests.Impl.Utility +{ + public class RequestHelper_tests + { + public class UrlEncode + { + [Fact] + public void Escapes_string() + { + var escaped = RequestHelper.UrlEncode("http://test# space 123/text?var=val&another=two"); + + escaped.ShouldBe("http%3A%2F%2Ftest%23+space+123%2Ftext%3Fvar%3Dval%26another%3Dtwo"); + } + + [Fact] + public void Canonicalizes_special_characters() + { + var escaped = RequestHelper.UrlEncode(" *~"); + var canonicalized = RequestHelper.UrlEncode(" *~", canonicalize: true); + + escaped.ShouldBe("+*%7E"); + canonicalized.ShouldBe("%20%2A~"); + } + + [Fact] + public void Canononicalizes_path_correctly() + { + var escaped = RequestHelper.UrlEncode("/"); + var canonicalized = RequestHelper.UrlEncode("/", canonicalize: true); + var canonicalizedWithPath = RequestHelper.UrlEncode("/", isPath: true, canonicalize: true); + + escaped.ShouldBe("%2F"); + canonicalized.ShouldBe("%2F"); + canonicalizedWithPath.ShouldBe("/"); + } + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index a1e0b1de..0049dd5e 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -102,9 +102,11 @@ + + diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs index 83b2c9f1..ee3b8ce5 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs @@ -16,14 +16,160 @@ // using System; +using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; using Stormpath.SDK.Api; +using Stormpath.SDK.Impl.Http.Support; +using Stormpath.SDK.Impl.Utility; namespace Stormpath.SDK.Impl.Http.Authentication { internal sealed class SAuthc1RequestAuthenticator : IRequestAuthenticator { + private static readonly string IDTerminator = "sauthc1_request"; + private static readonly string Algorithm = "HMAC-SHA-256"; + private static readonly string AuthenticationScheme = "SAuthc1"; + private static readonly string SAUTHC1Id = "sauthc1Id"; + private static readonly string SAUTHC1SignedHeaders = "sauthc1SignedHeaders"; + private static readonly string SAUTHC1Signature = "sauthc1Signature"; + private static readonly string Newline = "\n"; + void IRequestAuthenticator.Authenticate(HttpRequestMessage request, IClientApiKey apiKey) + { + var utcNow = DateTimeOffset.UtcNow; + var dateTimeString = Iso8601.Format(utcNow); + var dateString = utcNow.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture); + + var nonce = Guid.NewGuid().ToString(); + var requestBody = request.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + var requestBodyHash = ToHex(Hash(requestBody, Encoding.UTF8)); + var signedHeadersString = GetSortedHeaderKeys(request.Headers); + + var canonicalRequest = new StringBuilder() + .Append(request.Method.Method.ToUpper()) + .Append(Newline) + .Append(CanonicalizeResourcePath(request)) + .Append(Newline) + .Append(CanonicalizeQueryString(request)) + .Append(Newline) + .Append(CanonicalizeHeaders(request)) + .Append(Newline) + .Append(signedHeadersString) + .Append(Newline) + .Append(requestBodyHash) + .ToString(); + + var id = new StringBuilder() + .Append(apiKey.GetId()) + .Append("/") + .Append(dateString) + .Append("/") + .Append(nonce) + .Append("/") + .Append(IDTerminator) + .ToString(); + + var canonicalRequestHash = ToHex(Hash(canonicalRequest, Encoding.UTF8)); + var stringToSign = new StringBuilder() + .Append(Algorithm) + .Append(Newline) + .Append(dateTimeString) + .Append(Newline) + .Append(id) + .Append(Newline) + .Append(canonicalRequestHash) + .ToString(); + + byte[] secret = Encoding.UTF8.GetBytes($"{AuthenticationScheme}{apiKey.GetSecret()}"); + byte[] signDate = SignHmac256(dateString, secret, Encoding.UTF8); + byte[] signNonce = SignHmac256(nonce, signDate, Encoding.UTF8); + byte[] signTerminator = SignHmac256(IDTerminator, signNonce, Encoding.UTF8); + byte[] signature = SignHmac256(stringToSign, signTerminator, Encoding.UTF8); + + var authorizationHeader = new StringBuilder() + .Append(AuthenticationScheme) + .Append(SAUTHC1Id) + .Append("=") + .Append(id) + .Append(", ") + .Append(SAUTHC1SignedHeaders) + .Append("=") + .Append(signedHeadersString) + .Append(", ") + .Append(SAUTHC1Signature) + .Append("=") + .Append(ToHex(signature)) + .ToString(); + request.Headers.Add("Authorization", authorizationHeader); + } + + private static string ToHex(byte[] bytes) + { + return BitConverter.ToString(bytes) + .Replace("-", string.Empty) + .ToLower(); + } + + private static byte[] Hash(string text, Encoding encoding) + { + try + { + using (var hash = SHA256.Create()) + { + return hash.ComputeHash(encoding.GetBytes(text)); + } + } + catch (Exception e) + { + throw new RequestAuthenticationException("Unable to compute hash while signing request.", e); + } + } + + private static byte[] SignHmac256(string data, byte[] key, Encoding encoding) + { + return SignHmac256(encoding.GetBytes(data), key); + } + + private static byte[] SignHmac256(byte[] data, byte[] key) + { + try + { + using (var hmac = new HMACSHA256(key)) + { + return hmac.ComputeHash(data); + } + } + catch (Exception e) + { + throw new RequestAuthenticationException("Unable to calculate a request signature.", e); + } + } + + // Return all lowercase header names (keys) separated by semicolon + // e.g. header1;header2;header3 + private static string GetSortedHeaderKeys(HttpRequestHeaders headers) + { + var sortedKeys = headers + .Select(x => x.Key.ToLower()) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase); + + return string.Join(";", sortedKeys); + } + + private static string CanonicalizeQueryString(HttpRequestMessage request) + { + return new QueryString(request.RequestUri.Query).ToString(true); + } + + private static string CanonicalizeResourcePath(HttpRequestMessage request) + { + throw new NotImplementedException(); + } + + private static string CanonicalizeHeaders(HttpRequestMessage request) { throw new NotImplementedException(); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs index 4c993db8..7bc86549 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs @@ -70,7 +70,6 @@ private void SetupClient() client = new HttpClient(clientSettings); client.Timeout = TimeSpan.FromMilliseconds(connectionTimeout); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - // client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(UserAgentBuilder.GetUserAgent())); client.DefaultRequestHeaders.Add("User-Agent", UserAgentBuilder.GetUserAgent()); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Support/QueryString.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Support/QueryString.cs new file mode 100644 index 00000000..54d4b01e --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Support/QueryString.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using Stormpath.SDK.Impl.Utility; + +namespace Stormpath.SDK.Impl.Http.Support +{ + internal class QueryString + { + private readonly IList> queryStringItems; + + public QueryString() + { + queryStringItems = null; + } + + public QueryString(Uri uri) + : this(uri.Query) + { + } + + public QueryString(string queryString) + : this() + { + queryStringItems = Parse(queryString); + } + + public string ToString(bool canonical) + { + bool isEmpty = queryStringItems == null || queryStringItems.Count == 0; + if (isEmpty) + return string.Empty; + + var items = queryStringItems.Select(x => + { + var key = RequestHelper.UrlEncode(x.Key, false, canonical); + var value = RequestHelper.UrlEncode(x.Value, false, canonical); + return $"{key}={value}"; + }); + + return string.Join("&", items); + } + + public override string ToString() + { + return ToString(false); + } + + private static List> Parse(string queryString) + { + var resultItems = new List>(); + + if (queryString.Contains("?")) + queryString = queryString.Split('?')?[1]; + + if (string.IsNullOrEmpty(queryString)) + return resultItems; // empty + + foreach (var token in queryString.Split('&')) + { + var pair = token.Split('='); + + if (pair == null) + resultItems.Add(new KeyValuePair(token, null)); + + var key = pair[0]; + var value = string.Empty; + if (pair.Length > 1 && !string.IsNullOrEmpty(pair[1])) + value = pair[1]; + resultItems.Add(new KeyValuePair(key, value)); + } + + resultItems = resultItems + .OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase) + .ToList(); + return resultItems; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/RequestHelper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/RequestHelper.cs new file mode 100644 index 00000000..54a8e02d --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/RequestHelper.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Net; + +namespace Stormpath.SDK.Impl.Utility +{ + internal static class RequestHelper + { + public static string UrlEncode(string value, bool isPath = false, bool canonicalize = false) + { + if (string.IsNullOrEmpty(value)) + return string.Empty; + + var encoded = WebUtility.UrlEncode(value.ToLower()); + + if (canonicalize) + { + encoded = encoded + .Replace("+", "%20") + .Replace("*", "%2A") + .Replace("%7E", "~"); + + if (isPath) + encoded = encoded.Replace("%2F", "/"); + } + + return encoded; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index b268dbad..2a3c6704 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -87,6 +87,7 @@ + @@ -142,6 +143,7 @@ + From ceab9b19105965e977faa108c2add8d8cf8532c7 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 20 Aug 2015 14:01:12 -0700 Subject: [PATCH 078/238] Working SAuthc1 implementation --- .../BasicAuth_integration_tests.cs | 41 ++++ .../DefaultClient_tests.cs | 18 +- .../Integration_tests.cs | 16 +- .../SAuthc1_integration_tests.cs | 41 ++++ .../Stormpath.SDK.Tests.Integration.csproj | 2 + .../BasicRequestAuthenticator_tests.cs | 25 ++- .../SAuthc1RequestAuthenticator_tests.cs | 183 ++++++++++++++++++ .../Impl/Extensions_tests.cs | 40 ++++ .../Impl/QueryString_tests.cs | 4 +- .../Stormpath.SDK.Tests.csproj | 1 + .../Impl/Extensions/StringExtensions.cs | 13 ++ .../BasicRequestAuthenticator.cs | 9 +- .../SAuthc1RequestAuthenticator.cs | 129 ++++++++---- .../Impl/Http/NetHttpRequestExecutor.cs | 2 +- .../Stormpath.SDK/Impl/Utility/Iso8601.cs | 16 +- .../Impl/Utility/RequestHelper.cs | 3 +- 16 files changed, 471 insertions(+), 72 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests.Integration/BasicAuth_integration_tests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests.Integration/SAuthc1_integration_tests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/SAuthc1RequestAuthenticator_tests.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/BasicAuth_integration_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/BasicAuth_integration_tests.cs new file mode 100644 index 00000000..a7514774 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/BasicAuth_integration_tests.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Stormpath.SDK.Api; +using Stormpath.SDK.Client; + +namespace Stormpath.SDK.Tests.Integration +{ + // TODO make this much more clean with xUnit 2 theories + public class BasicAuth_integration_tests : Integration_tests + { + public BasicAuth_integration_tests() + : base(BuildClient(GetApiKey())) + { + } + + private static IClient BuildClient(IClientApiKey apiKey) + { + var client = Clients + .Builder() + .SetApiKey(apiKey) + .SetAuthenticationScheme(AuthenticationScheme.Basic) + .Build(); + return client; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs index a07324ed..b6623107 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs @@ -21,7 +21,23 @@ namespace Stormpath.SDK.Tests.Integration { - public class DefaultClient_tests : Integration_tests + public class DefaultClient_Basic_tests : BasicAuth_integration_tests + { + [Fact] + public async Task Getting_current_tenant() + { + var tenant = await harness.Client.GetCurrentTenantAsync(); + + tenant.ShouldNotBe(null); + tenant.Href.ShouldNotBe(null); + tenant.Name.ShouldNotBe(null); + + // TODO - verify actual tenant data? + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single class", Justification = "")] + public class DefaultClient_SAuthc1_tests : SAuthc1_integration_tests { [Fact] public async Task Getting_current_tenant() diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Integration_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Integration_tests.cs index 5419dfc6..28a10c3c 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Integration_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Integration_tests.cs @@ -26,19 +26,17 @@ public abstract class Integration_tests [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields must be private", Justification = "Reviewed.")] protected readonly IntegrationHarness harness; - public Integration_tests() + public Integration_tests(IClient client) + { + harness = new IntegrationHarness(client); + } + + protected static IClientApiKey GetApiKey() { // Expect that API keys are in environment variables. (works with travis-ci) var apiKey = ClientApiKeys.Builder().Build(); apiKey.IsValid().ShouldBe(true); - - var client = Clients - .Builder() - .SetApiKey(apiKey) - .SetAuthenticationScheme(AuthenticationScheme.Basic) // TODO - .Build(); - - harness = new IntegrationHarness(client); + return apiKey; } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/SAuthc1_integration_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/SAuthc1_integration_tests.cs new file mode 100644 index 00000000..0f50bfec --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/SAuthc1_integration_tests.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Stormpath.SDK.Api; +using Stormpath.SDK.Client; + +namespace Stormpath.SDK.Tests.Integration +{ + // TODO make this much more clean with xUnit 2 theories + public class SAuthc1_integration_tests : Integration_tests + { + public SAuthc1_integration_tests() + : base(BuildClient(GetApiKey())) + { + } + + private static IClient BuildClient(IClientApiKey apiKey) + { + var client = Clients + .Builder() + .SetApiKey(apiKey) + .SetAuthenticationScheme(AuthenticationScheme.SAuthc1) + .Build(); + return client; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj index cda120da..c215342d 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj @@ -60,6 +60,8 @@ + + diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/BasicRequestAuthenticator_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/BasicRequestAuthenticator_tests.cs index 701cadb6..c93dd732 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/BasicRequestAuthenticator_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/BasicRequestAuthenticator_tests.cs @@ -31,7 +31,7 @@ namespace Stormpath.SDK.Tests.Impl.Authentication { public class BasicRequestAuthenticator_tests { - private readonly IRequestAuthenticator authenticator; + private readonly BasicRequestAuthenticator authenticator; private readonly IClientApiKey apiKey; private readonly string fakeApiKeyId = "foo-api-key"; @@ -44,20 +44,27 @@ public BasicRequestAuthenticator_tests() } [Fact] - public void Authenticates_request_with_basic_scheme() + public void Adds_XStormpathDate_header() { var myRequest = new HttpRequestMessage(); + var now = new DateTimeOffset(2015, 08, 01, 06, 30, 00, TimeSpan.Zero); - authenticator.Authenticate(myRequest, apiKey); + authenticator.AuthenticateCore(myRequest, apiKey, now); - // X-Stormpath-Date -> now in UTC + // X-Stormpath-Date -> current time in UTC var XStormpathDateHeader = Iso8601.Parse(myRequest.Headers.GetValues("X-Stormpath-Date").Single()); - XStormpathDateHeader.Year.ShouldBe(DateTimeOffset.UtcNow.Year); - XStormpathDateHeader.Month.ShouldBe(DateTimeOffset.UtcNow.Month); - XStormpathDateHeader.Day.ShouldBe(DateTimeOffset.UtcNow.Day); - XStormpathDateHeader.Offset.ShouldBe(TimeSpan.Zero); + XStormpathDateHeader.ShouldBe(now); + } + + [Fact] + public void Adds_Basic_authorization_header() + { + var myRequest = new HttpRequestMessage(); + var now = new DateTimeOffset(2015, 08, 01, 06, 30, 00, TimeSpan.Zero); + + authenticator.AuthenticateCore(myRequest, apiKey, now); - // Authorization -> Basic [base64 stuff] + // Authorization: "Basic [base64 stuff]" var authenticationHeader = myRequest.Headers.Authorization; authenticationHeader.Scheme.ShouldBe("Basic"); authenticationHeader.Parameter.FromBase64(Encoding.UTF8).ShouldBe($"{fakeApiKeyId}:{fakeApiKeySecret}"); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/SAuthc1RequestAuthenticator_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/SAuthc1RequestAuthenticator_tests.cs new file mode 100644 index 00000000..f1ed6bbc --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Authentication/SAuthc1RequestAuthenticator_tests.cs @@ -0,0 +1,183 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using Shouldly; +using Stormpath.SDK.Api; +using Stormpath.SDK.Impl.Api; +using Stormpath.SDK.Impl.Extensions; +using Stormpath.SDK.Impl.Http.Authentication; +using Stormpath.SDK.Impl.Utility; +using Xunit; + +namespace Stormpath.SDK.Tests.Impl.Authentication +{ + public class SAuthc1RequestAuthenticator_tests + { + private readonly SAuthc1RequestAuthenticator authenticator; + private readonly DateTimeOffset fakeNow; + private readonly string fakeNonce = "b3a8dfee-af7a-4dc0-b008-b2433040dfbd"; + + public SAuthc1RequestAuthenticator_tests() + { + authenticator = new SAuthc1RequestAuthenticator(); + fakeNow = new DateTimeOffset(2015, 08, 02, 12, 30, 59, TimeSpan.Zero); + } + + [Fact] + public void Throws_for_empty_request_URI() + { + var apiKey = new ClientApiKey("foo", "bar"); + var myRequest = new HttpRequestMessage(HttpMethod.Get, string.Empty); + + Should.Throw(() => + { + authenticator.AuthenticateCore(myRequest, apiKey, fakeNow, fakeNonce); + }); + } + + [Fact] + public void Adds_XStormpathDate_header() + { + var apiKey = new ClientApiKey("foo", "bar"); + var myRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.foo.bar/stuff"); + + authenticator.AuthenticateCore(myRequest, apiKey, fakeNow, fakeNonce); + + // X-Stormpath-Date -> current time in UTC + var XStormpathDateHeader = Iso8601.Parse(myRequest.Headers.GetValues("X-Stormpath-Date").Single()); + XStormpathDateHeader.ShouldBe(fakeNow); + } + + [Fact] + public void Adds_Host_header() + { + var apiKey = new ClientApiKey("foo", "bar"); + var myRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.foo.bar/stuff"); + + authenticator.AuthenticateCore(myRequest, apiKey, fakeNow, fakeNonce); + + // Host: [hostname] + myRequest.Headers.Host.ShouldBe("api.foo.bar"); + } + + [Fact] + public void Adds_Host_header_with_nondefault_port() + { + var apiKey = new ClientApiKey("foo", "bar"); + var myRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.foo.bar:8088"); + + authenticator.AuthenticateCore(myRequest, apiKey, fakeNow, fakeNonce); + + // Host: [hostname] + myRequest.Headers.Host.ShouldBe("api.foo.bar:8088"); + } + + [Fact] + public void Adds_SAuthc1_authorization_header() + { + IClientApiKey apiKey = new ClientApiKey("myAppId", "super-secret"); + var myRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.stormpath.com/v1/accounts"); + + authenticator.AuthenticateCore(myRequest, apiKey, fakeNow, fakeNonce); + + // Authorization: "SAuthc1 [signed hash]" + var authenticationHeader = myRequest.Headers.Authorization; + authenticationHeader.Scheme.ShouldBe("SAuthc1"); + authenticationHeader.Parameter.ShouldNotBe(null); + + // Format "sauthc1Id=[id string], sauthc1SignedHeaders=[host;x-stormpath-date;...], sauthc1Signature=[signature in hex]" + var parts = authenticationHeader.Parameter.Split(' '); + var sauthc1Id = parts[0].TrimEnd(',').SplitToKeyValuePair('='); + var sauthc1SignedHeaders = parts[1].TrimEnd(',').SplitToKeyValuePair('='); + var sauthc1Signature = parts[2].SplitToKeyValuePair('='); + var dateString = fakeNow.ToString("yyyyMMdd", CultureInfo.InvariantCulture); + + sauthc1Id.Key.ShouldBe("sauthc1Id"); + sauthc1Id.Value.ShouldBe($"{apiKey.GetId()}/{dateString}/{fakeNonce}/sauthc1_request"); + + sauthc1SignedHeaders.Key.ShouldBe("sauthc1SignedHeaders"); + sauthc1SignedHeaders.Value.ShouldBe("host;x-stormpath-date"); + + sauthc1Signature.Key.ShouldBe("sauthc1Signature"); + sauthc1Signature.Value.ShouldNotBe(null); + } + + [Fact] + public void Should_authenticate_request_without_query_params() + { + IClientApiKey apiKey = new ClientApiKey("MyId", "Shush!"); + var request = new HttpRequestMessage(HttpMethod.Get, "https://api.stormpath.com/v1/"); + var now = new DateTimeOffset(2013, 7, 1, 0, 0, 0, TimeSpan.Zero); + var nonce = "a43a9d25-ab06-421e-8605-33fd1e760825"; + + authenticator.AuthenticateCore(request, apiKey, now, nonce); + + var sauthc1Id = request.Headers.Authorization.Parameter.Split(' ')[0]; + var sauthc1SignedHeaders = request.Headers.Authorization.Parameter.Split(' ')[1]; + var sauthc1Signature = request.Headers.Authorization.Parameter.Split(' ')[2]; + + request.Headers.Authorization.Scheme.ShouldBe("SAuthc1"); + sauthc1Id.ShouldBe("sauthc1Id=MyId/20130701/a43a9d25-ab06-421e-8605-33fd1e760825/sauthc1_request,"); + sauthc1SignedHeaders.ShouldBe("sauthc1SignedHeaders=host;x-stormpath-date,"); + sauthc1Signature.ShouldBe("sauthc1Signature=990a95aabbcbeb53e48fb721f73b75bd3ae025a2e86ad359d08558e1bbb9411c"); + } + + [Fact] + public void Should_authenticate_request_with_query_params() + { + IClientApiKey apiKey = new ClientApiKey("MyId", "Shush!"); + var request = new HttpRequestMessage(HttpMethod.Get, "https://api.stormpath.com/v1/directories?orderBy=name+asc"); + var now = new DateTimeOffset(2013, 7, 1, 0, 0, 0, TimeSpan.Zero); + var nonce = "a43a9d25-ab06-421e-8605-33fd1e760825"; + + authenticator.AuthenticateCore(request, apiKey, now, nonce); + + var sauthc1Id = request.Headers.Authorization.Parameter.Split(' ')[0]; + var sauthc1SignedHeaders = request.Headers.Authorization.Parameter.Split(' ')[1]; + var sauthc1Signature = request.Headers.Authorization.Parameter.Split(' ')[2]; + + request.Headers.Authorization.Scheme.ShouldBe("SAuthc1"); + sauthc1Id.ShouldBe("sauthc1Id=MyId/20130701/a43a9d25-ab06-421e-8605-33fd1e760825/sauthc1_request,"); + sauthc1SignedHeaders.ShouldBe("sauthc1SignedHeaders=host;x-stormpath-date,"); + sauthc1Signature.ShouldBe("sauthc1Signature=fc04c5187cc017bbdf9c0bb743a52a9487ccb91c0996267988ceae3f10314176"); + } + + [Fact] + public void Should_authenticate_request_with_multiple_query_params() + { + IClientApiKey apiKey = new ClientApiKey("MyId", "Shush!"); + var request = new HttpRequestMessage(HttpMethod.Get, "https://api.stormpath.com/v1/applications/77JnfFiREjdfQH0SObMfjI/groups?q=group&limit=25&offset=25"); + var now = new DateTimeOffset(2013, 7, 1, 0, 0, 0, TimeSpan.Zero); + var nonce = "a43a9d25-ab06-421e-8605-33fd1e760825"; + + authenticator.AuthenticateCore(request, apiKey, now, nonce); + + var sauthc1Id = request.Headers.Authorization.Parameter.Split(' ')[0]; + var sauthc1SignedHeaders = request.Headers.Authorization.Parameter.Split(' ')[1]; + var sauthc1Signature = request.Headers.Authorization.Parameter.Split(' ')[2]; + + request.Headers.Authorization.Scheme.ShouldBe("SAuthc1"); + sauthc1Id.ShouldBe("sauthc1Id=MyId/20130701/a43a9d25-ab06-421e-8605-33fd1e760825/sauthc1_request,"); + sauthc1SignedHeaders.ShouldBe("sauthc1SignedHeaders=host;x-stormpath-date,"); + sauthc1Signature.ShouldBe("sauthc1Signature=e30a62c0d03ca6cb422e66039786865f3eb6269400941ede6226760553a832d3"); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Extensions_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Extensions_tests.cs index b8f131ad..03708cbd 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Extensions_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Extensions_tests.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System; using Shouldly; using Stormpath.SDK.Impl.Extensions; using Xunit; @@ -85,5 +86,44 @@ public void Returns_Zm9vYmFy_for_foobar() "Zm9vYmFy".FromBase64(System.Text.Encoding.UTF8).ShouldBe("foobar"); } } + + public class String_SplitToKeyValuePair + { + [Fact] + public void Throws_when_string_is_null() + { + Should.Throw(() => + { + var bad = ((string)null).SplitToKeyValuePair('='); + }); + } + + [Fact] + public void Throws_when_string_does_not_contain_separator() + { + Should.Throw(() => + { + var bad = "one,two".SplitToKeyValuePair('='); + }); + } + + [Fact] + public void Throws_when_string_does_not_have_two_sub_items() + { + Should.Throw(() => + { + var bad = "one=two=three".SplitToKeyValuePair('='); + }); + } + + [Fact] + public void Returns_split_items_as_key_value_pair() + { + var args = "foo=bar".SplitToKeyValuePair('='); + + args.Key.ShouldBe("foo"); + args.Value.ShouldBe("bar"); + } + } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/QueryString_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/QueryString_tests.cs index 73264262..00113eb1 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/QueryString_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/QueryString_tests.cs @@ -43,11 +43,11 @@ public void Creating_from_string() } [Fact] - public void Keys_and_values_are_lowercase() + public void Keys_and_value_case_is_preserved() { var qs = new QueryString(new Uri("http://foo.bar/baz?TEST=foo&bar=BAZ")); - qs.ToString().ShouldBe("bar=baz&test=foo"); + qs.ToString().ShouldBe("bar=BAZ&TEST=foo"); } [Fact] diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 0049dd5e..98b925d3 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -78,6 +78,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs index 720bef3d..1d214640 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Extensions/StringExtensions.cs @@ -16,6 +16,7 @@ // using System; +using System.Collections.Generic; using System.Text; namespace Stormpath.SDK.Impl.Extensions @@ -44,5 +45,17 @@ public static string FromBase64(this string base64Source, Encoding encoding) return encoding.GetString(Convert.FromBase64String(base64Source)); } + + public static KeyValuePair SplitToKeyValuePair(this string source, char separator) + { + if (string.IsNullOrEmpty(source) || !source.Contains(separator.ToString())) + throw new FormatException($"Input string is not a '{separator}'-separated string."); + + var pair = source.Split(separator); + if (pair.Length != 2) + throw new FormatException($"Input string is not a key-value pair."); + + return new KeyValuePair(pair[0], pair[1]); + } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs index 97885e90..da3f67c6 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/BasicRequestAuthenticator.cs @@ -31,8 +31,13 @@ internal sealed class BasicRequestAuthenticator : IRequestAuthenticator void IRequestAuthenticator.Authenticate(HttpRequestMessage request, IClientApiKey apiKey) { - var utcNow = DateTimeOffset.UtcNow; - request.Headers.Add(StormpathDateHeaderName, Iso8601.Format(utcNow)); + var now = DateTimeOffset.UtcNow; + AuthenticateCore(request, apiKey, now); + } + + internal void AuthenticateCore(HttpRequestMessage request, IClientApiKey apiKey, DateTimeOffset now) + { + request.Headers.Add(StormpathDateHeaderName, Iso8601.Format(now, withSeparators: false)); var authorizationHeaderContent = $"{apiKey.GetId()}:{apiKey.GetSecret()}"; var authorizationHeader = authorizationHeaderContent.ToBase64(Encoding.UTF8); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs index ee3b8ce5..f70645a5 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/Authentication/SAuthc1RequestAuthenticator.cs @@ -16,6 +16,7 @@ // using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -29,6 +30,7 @@ namespace Stormpath.SDK.Impl.Http.Authentication { internal sealed class SAuthc1RequestAuthenticator : IRequestAuthenticator { + private static readonly string StormpathDateHeaderName = "X-Stormpath-Date"; private static readonly string IDTerminator = "sauthc1_request"; private static readonly string Algorithm = "HMAC-SHA-256"; private static readonly string AuthenticationScheme = "SAuthc1"; @@ -39,36 +41,58 @@ internal sealed class SAuthc1RequestAuthenticator : IRequestAuthenticator void IRequestAuthenticator.Authenticate(HttpRequestMessage request, IClientApiKey apiKey) { - var utcNow = DateTimeOffset.UtcNow; - var dateTimeString = Iso8601.Format(utcNow); - var dateString = utcNow.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture); - + var now = DateTimeOffset.UtcNow; var nonce = Guid.NewGuid().ToString(); - var requestBody = request.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + AuthenticateCore(request, apiKey, now, nonce); + } + + internal void AuthenticateCore(HttpRequestMessage request, IClientApiKey apiKey, DateTimeOffset now, string nonce) + { + if (string.IsNullOrEmpty(request?.RequestUri?.AbsoluteUri)) + throw new RequestAuthenticationException("URL must not be empty."); + + var uri = request.RequestUri; + if (!uri.IsAbsoluteUri) + throw new RequestAuthenticationException("URL must be an absolute path."); + + var relativeResourcePath = uri.AbsolutePath; + + var timestamp = Iso8601.Format(now, withSeparators: false); + var dateStamp = now.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture); + + // Add HOST header before signing + var hostHeader = uri.Host; + if (!uri.IsDefaultPort) + hostHeader = $"{hostHeader}:{uri.Port}"; + request.Headers.Host = hostHeader; + + // Add X-Stormpath-Date before signing + request.Headers.Add(StormpathDateHeaderName, timestamp); + + var requestBody = string.Empty; + if (request.Content != null) + requestBody = request.Content.ReadAsStringAsync().GetAwaiter().GetResult(); var requestBodyHash = ToHex(Hash(requestBody, Encoding.UTF8)); - var signedHeadersString = GetSortedHeaderKeys(request.Headers); + var sortedHeaderKeys = GetSortedHeaderNames(request.Headers); var canonicalRequest = new StringBuilder() .Append(request.Method.Method.ToUpper()) .Append(Newline) - .Append(CanonicalizeResourcePath(request)) + .Append(CanonicalizeResourcePath(relativeResourcePath)) .Append(Newline) .Append(CanonicalizeQueryString(request)) .Append(Newline) .Append(CanonicalizeHeaders(request)) .Append(Newline) - .Append(signedHeadersString) + .Append(sortedHeaderKeys) .Append(Newline) .Append(requestBodyHash) .ToString(); var id = new StringBuilder() - .Append(apiKey.GetId()) - .Append("/") - .Append(dateString) - .Append("/") - .Append(nonce) - .Append("/") + .Append(apiKey.GetId()).Append("/") + .Append(dateStamp).Append("/") + .Append(nonce).Append("/") .Append(IDTerminator) .ToString(); @@ -76,34 +100,27 @@ void IRequestAuthenticator.Authenticate(HttpRequestMessage request, IClientApiKe var stringToSign = new StringBuilder() .Append(Algorithm) .Append(Newline) - .Append(dateTimeString) + .Append(timestamp) .Append(Newline) .Append(id) .Append(Newline) .Append(canonicalRequestHash) .ToString(); - byte[] secret = Encoding.UTF8.GetBytes($"{AuthenticationScheme}{apiKey.GetSecret()}"); - byte[] signDate = SignHmac256(dateString, secret, Encoding.UTF8); - byte[] signNonce = SignHmac256(nonce, signDate, Encoding.UTF8); - byte[] signTerminator = SignHmac256(IDTerminator, signNonce, Encoding.UTF8); - byte[] signature = SignHmac256(stringToSign, signTerminator, Encoding.UTF8); - - var authorizationHeader = new StringBuilder() - .Append(AuthenticationScheme) - .Append(SAUTHC1Id) - .Append("=") - .Append(id) - .Append(", ") - .Append(SAUTHC1SignedHeaders) - .Append("=") - .Append(signedHeadersString) - .Append(", ") - .Append(SAUTHC1Signature) - .Append("=") - .Append(ToHex(signature)) + var secretFormat = $"{AuthenticationScheme}{apiKey.GetSecret()}"; + byte[] secret = Encoding.UTF8.GetBytes(secretFormat); + byte[] signedDate = SignHmac256(dateStamp, secret, Encoding.UTF8); + byte[] signedNonce = SignHmac256(nonce, signedDate, Encoding.UTF8); + byte[] signedTerminator = SignHmac256(IDTerminator, signedNonce, Encoding.UTF8); + byte[] signature = SignHmac256(stringToSign, signedTerminator, Encoding.UTF8); + var signatureHex = ToHex(signature); + + var authorizationHeaderValue = new StringBuilder() + .Append(SAUTHC1Id).Append("=").Append(id).Append(", ") + .Append(SAUTHC1SignedHeaders).Append("=").Append(sortedHeaderKeys).Append(", ") + .Append(SAUTHC1Signature).Append("=").Append(signatureHex) .ToString(); - request.Headers.Add("Authorization", authorizationHeader); + request.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationScheme, authorizationHeaderValue); } private static string ToHex(byte[] bytes) @@ -150,28 +167,56 @@ private static byte[] SignHmac256(byte[] data, byte[] key) // Return all lowercase header names (keys) separated by semicolon // e.g. header1;header2;header3 - private static string GetSortedHeaderKeys(HttpRequestHeaders headers) + private static string GetSortedHeaderNames(HttpRequestHeaders headers) { var sortedKeys = headers - .Select(x => x.Key.ToLower()) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase); + .OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase) + .Select(x => x.Key.ToLower()); return string.Join(";", sortedKeys); } private static string CanonicalizeQueryString(HttpRequestMessage request) { - return new QueryString(request.RequestUri.Query).ToString(true); + return new QueryString(request.RequestUri.Query) + .ToString(canonical: true); } - private static string CanonicalizeResourcePath(HttpRequestMessage request) + private static string CanonicalizeResourcePath(string relativeResourcePath) { - throw new NotImplementedException(); + if (string.IsNullOrEmpty(relativeResourcePath)) + return "/"; + return RequestHelper.UrlEncode(relativeResourcePath, isPath: true, canonicalize: true); } private static string CanonicalizeHeaders(HttpRequestMessage request) { - throw new NotImplementedException(); + var buffer = new StringBuilder(); + + var sortedHeaders = request.Headers + .OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase); + + foreach (var header in sortedHeaders) + { + buffer.Append(header.Key.ToLower()); + buffer.Append(":"); + + if (header.Value.Any()) + { + var first = true; + foreach (var value in header.Value) + { + if (!first) + buffer.Append(","); + buffer.Append(value); + first = false; + } + } + + buffer.Append(Newline); + } + + return buffer.ToString(); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs index 7bc86549..b4e7b16e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs @@ -112,7 +112,7 @@ private bool IsRedirect(HttpResponseMessage response) response.StatusCode == HttpStatusCode.MovedPermanently || // 301 response.StatusCode == HttpStatusCode.Redirect || // 302 response.StatusCode == HttpStatusCode.TemporaryRedirect; // 307 - bool hasNewLocation = !string.IsNullOrEmpty(response.Headers.Location?.AbsolutePath); + bool hasNewLocation = !string.IsNullOrEmpty(response.Headers.Location?.AbsoluteUri); return moved && hasNewLocation; } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs index bd2ce610..fd55e94e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/Iso8601.cs @@ -22,20 +22,26 @@ namespace Stormpath.SDK.Impl.Utility { internal static class Iso8601 { - public static string Format(DateTimeOffset dto) + private static readonly string FormatWithSeparators = "yyyy-MM-ddTHH:mm:ssZ"; + private static readonly string FormatWithoutSeparators = "yyyyMMddTHHmmssZ"; + + private static readonly string ParsePatternWithSeparators = "yyyy-MM-dd'T'HH:mm:ss.FFFK"; + private static readonly string ParsePatternWithoutSeparators = "yyyyMMdd'T'HHmmssFFFK"; + + public static string Format(DateTimeOffset dto, bool withSeparators = true) { return dto.UtcDateTime.ToString( - "yyyy-MM-ddTHH:mm:ssZ", + withSeparators ? FormatWithSeparators : FormatWithoutSeparators, CultureInfo.InvariantCulture); } public static DateTimeOffset Parse(string iso8601String) { - string pattern = "yyyy-MM-dd'T'HH:mm:ss.FFFK"; return DateTimeOffset.ParseExact( iso8601String, - pattern, - CultureInfo.InvariantCulture); + new string[] { ParsePatternWithSeparators, ParsePatternWithoutSeparators }, + CultureInfo.InvariantCulture, + DateTimeStyles.None); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/RequestHelper.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/RequestHelper.cs index 54a8e02d..1d435051 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/RequestHelper.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/RequestHelper.cs @@ -26,11 +26,12 @@ public static string UrlEncode(string value, bool isPath = false, bool canonical if (string.IsNullOrEmpty(value)) return string.Empty; - var encoded = WebUtility.UrlEncode(value.ToLower()); + var encoded = WebUtility.UrlEncode(value); if (canonicalize) { encoded = encoded + .Replace("%2B", "+") .Replace("+", "%20") .Replace("*", "%2A") .Replace("%7E", "~"); From 959fe5f0db718a2c66406e34f9cb131c6cfa6587 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Fri, 21 Aug 2015 06:41:31 -0700 Subject: [PATCH 079/238] Working Application collection materialization --- Stormpath.SDK/Stormpath.Demo/Program.cs | 19 +-- .../DefaultClient_tests.cs | 26 ++- .../DefaultTenant_tests.cs | 47 ++++++ .../Stormpath.SDK.Tests.Integration.csproj | 1 + .../Fakes/FakeDataStore.cs | 4 +- .../Stormpath.SDK.Tests/Fakes/FakeJson.cs | 156 ++++++++++++++++++ .../Helpers/CollectionTestHarness.cs | 16 +- .../Helpers/LinqAssertExtensions.cs | 15 +- ...ectionResourceQueryable_iteration_tests.cs | 18 +- .../Impl/InternalDataStore_tests.cs | 51 ++++++ .../Impl/Linq/Expand_extension_tests.cs | 18 +- .../Impl/Linq/Filter_extension_tests.cs | 4 +- .../Impl/Linq/FirstAsync_tests.cs | 8 +- .../Impl/Linq/Linq_tests.cs | 16 +- .../Impl/Linq/OrderBy_tests.cs | 6 +- .../Impl/Linq/Result_operators_tests.cs | 15 +- .../Impl/Linq/SingleAsync_tests.cs | 12 +- .../Impl/Linq/Skip_tests.cs | 8 +- .../Impl/Linq/SyncAdapter_tests.cs | 6 +- .../Impl/Linq/Take_tests.cs | 10 +- .../Impl/Linq/ToListAsync_tests.cs | 19 ++- .../Impl/Linq/Unsupported_filters_tests.cs | 32 ++-- .../Impl/Linq/Where_tests.cs | 38 ++--- .../Impl/Linq/Within_extension_tests.cs | 12 +- .../Stormpath.SDK/Account/AccountStatus.cs | 5 - .../Application/ApplicationStatus.cs | 41 ++++- .../Stormpath.SDK/Application/IApplication.cs | 4 +- .../Impl/Application/DefaultApplication.cs | 88 ++++++++++ .../Impl/Client/DefaultClient.cs | 3 +- .../Impl/DataStore/DefaultResourceFactory.cs | 85 ++++++++-- .../Impl/DataStore/IDataStore.cs | 7 +- .../Impl/DataStore/IResourceFactory.cs | 4 +- .../Impl/DataStore/InternalDataStore.cs | 7 +- .../Impl/DataStore/JsonNetMapMarshaller.cs | 73 +++++++- .../Impl/DataStore/StormpathJsonConverter.cs | 57 ++++++- .../Linq/CollectionResourceQueryExecutor.cs | 12 +- .../Impl/Resource/AbstractResource.cs | 6 +- .../Resource/CollectionResourceQueryable.cs | 17 +- ...sePageDto.cs => CollectionResponsePage.cs} | 18 +- .../Impl/Tenant/DefaultTenant.cs | 6 +- .../Stormpath.SDK/Stormpath.SDK.csproj | 3 +- .../Stormpath.SDK/Tenant/ITenantActions.cs | 3 +- 42 files changed, 780 insertions(+), 216 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultTenant_tests.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.cs rename Stormpath.SDK/Stormpath.SDK/Impl/Resource/{CollectionResponsePageDto.cs => CollectionResponsePage.cs} (64%) diff --git a/Stormpath.SDK/Stormpath.Demo/Program.cs b/Stormpath.SDK/Stormpath.Demo/Program.cs index 479489d3..40854253 100644 --- a/Stormpath.SDK/Stormpath.Demo/Program.cs +++ b/Stormpath.SDK/Stormpath.Demo/Program.cs @@ -1,9 +1,11 @@ using System; using System.Threading.Tasks; using System.Linq; +using Stormpath.SDK; using Stormpath.SDK.Api; using Stormpath.SDK.Client; using System.Threading; +using Stormpath.SDK.Application; namespace Stormpath.Demo { @@ -45,18 +47,13 @@ static async Task MainAsync(CancellationToken ct) var tenant = await client.GetCurrentTenantAsync(); Console.WriteLine($"Current tenant is: {tenant.Name}"); - // Get - var myApp = tenant.GetApplications() - .Where(x => x.Name == "My Application") - .First(); - // List applications - //Console.WriteLine("Tenant applications:"); - //var applications = await tenant.GetApplicationsAsync(); - //foreach (var app in applications) - //{ - // Console.WriteLine("{0}\t{1}", app.Name, app.Status == ApplicationStatus.Enabled ? "enabled" : "disabled"); - //} + Console.WriteLine("Tenant applications:"); + var applications = await tenant.GetApplications().ToListAsync(); + foreach (var app in applications) + { + Console.WriteLine("{0}\t{1}", app.Name, app.Status == ApplicationStatus.Enabled ? "enabled" : "disabled"); + } //// Add some users //Console.WriteLine("\nAdding users to '{0}'...", myApp.Name); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs index b6623107..374fee05 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultClient_tests.cs @@ -21,10 +21,10 @@ namespace Stormpath.SDK.Tests.Integration { - public class DefaultClient_Basic_tests : BasicAuth_integration_tests + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single class", Justification = "Reviewed.")] + public class DefaultClient_tests { - [Fact] - public async Task Getting_current_tenant() + private static async Task Impl_Getting_current_tenant(IntegrationHarness harness) { var tenant = await harness.Client.GetCurrentTenantAsync(); @@ -34,21 +34,17 @@ public async Task Getting_current_tenant() // TODO - verify actual tenant data? } - } - [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single class", Justification = "")] - public class DefaultClient_SAuthc1_tests : SAuthc1_integration_tests - { - [Fact] - public async Task Getting_current_tenant() + public class DefaultClient_Basic_tests : BasicAuth_integration_tests { - var tenant = await harness.Client.GetCurrentTenantAsync(); - - tenant.ShouldNotBe(null); - tenant.Href.ShouldNotBe(null); - tenant.Name.ShouldNotBe(null); + [Fact] + public async Task Getting_current_tenant() => await Impl_Getting_current_tenant(harness); + } - // TODO - verify actual tenant data? + public class DefaultClient_SAuthc1_tests : SAuthc1_integration_tests + { + [Fact] + public async Task Getting_current_tenant() => await Impl_Getting_current_tenant(harness); } } } \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultTenant_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultTenant_tests.cs new file mode 100644 index 00000000..a9608027 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/DefaultTenant_tests.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Stormpath.SDK.Tests.Integration +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single class", Justification = "Reviewed.")] + public class DefaultTenant_tests + { + private static async Task Impl_Getting_tenant_applications(IntegrationHarness harness) + { + var tenant = await harness.Client.GetCurrentTenantAsync(); + var applications = await tenant.GetApplications().ToListAsync(); + + applications.Count.ShouldNotBe(0); + } + + public class DefaultClient_Basic_tests : BasicAuth_integration_tests + { + [Fact] + public async Task Getting_tenant_applications() => await Impl_Getting_tenant_applications(harness); + } + + public class DefaultClient_SAuthc1_tests : SAuthc1_integration_tests + { + [Fact] + public async Task Getting_tenant_applications() => await Impl_Getting_tenant_applications(harness); + } + } +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj index c215342d..a0910b0c 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj @@ -60,6 +60,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeDataStore.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeDataStore.cs index afe739f7..dd6c7cac 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeDataStore.cs @@ -48,7 +48,7 @@ public IEnumerable GetCalls() return calls; } - async Task> IDataStore.GetCollectionAsync(string href, CancellationToken cancellationToken) + async Task> IDataStore.GetCollectionAsync(string href, CancellationToken cancellationToken) { bool typesMatch = typeof(T) == typeof(TType); if (!typesMatch) @@ -61,7 +61,7 @@ async Task> IDataStore.GetCollectionAsync(string var limit = GetLimitFromUrlString(href) ?? defaultLimit; var offset = GetOffsetFromUrlString(href) ?? defaultOffset; - return new CollectionResponsePageDto() + return new CollectionResponsePage() { Href = href, Items = this.Items.OfType().Skip(offset).Take(limit).ToList(), diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeJson.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeJson.cs index b9d64b58..2b2c37d8 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeJson.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeJson.cs @@ -107,5 +107,161 @@ public static class FakeJson #endregion + #region Application 'Lightsabers Galore' + + public static readonly string Application = @" + { + ""accountStoreMappings"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/accountStoreMappings"" + }, + ""accounts"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/accounts"" + }, + ""apiKeys"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/apiKeys"" + }, + ""authTokens"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/authTokens"" + }, + ""createdAt"": ""2015-07-21T23:50:49.563Z"", + ""customData"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/customData"" + }, + ""defaultAccountStoreMapping"": { + ""href"": ""https://api.stormpath.com/v1/accountStoreMappings/foobarASM"" + }, + ""defaultGroupStoreMapping"": { + ""href"": ""https://api.stormpath.com/v1/accountStoreMappings/foobarASM"" + }, + ""description"": ""This application is so awesome, you don't even know."", + ""groups"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/groups"" + }, + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication"", + ""loginAttempts"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/loginAttempts"" + }, + ""modifiedAt"": ""2015-07-21T23:50:49.622Z"", + ""name"": ""Lightsabers Galore"", + ""oAuthPolicy"": { + ""href"": ""https://api.stormpath.com/v1/oAuthPolicies/foobarApplication"" + }, + ""passwordResetTokens"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/passwordResetTokens"" + }, + ""status"": ""ENABLED"", + ""tenant"": { + ""href"": ""https://api.stormpath.com/v1/tenants/foobarTenant"" + }, + ""verificationEmails"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/verificationEmails"" + } + }"; + + #endregion + + #region Application list + + public static readonly string ApplicationList = @" +{ + ""href"": ""https://api.stormpath.com/v1/tenants/foobarTenant/applications"", + ""items"": [ + { + ""accountStoreMappings"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication/accountStoreMappings"" + }, + ""accounts"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication1/accounts"" + }, + ""apiKeys"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication1/apiKeys"" + }, + ""authTokens"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication1/authTokens"" + }, + ""createdAt"": ""2015-07-21T23:50:49.563Z"", + ""customData"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication1/customData"" + }, + ""defaultAccountStoreMapping"": { + ""href"": ""https://api.stormpath.com/v1/accountStoreMappings/foobarASM1"" + }, + ""defaultGroupStoreMapping"": { + ""href"": ""https://api.stormpath.com/v1/accountStoreMappings/foobarASM1"" + }, + ""description"": ""This application was automatically created for you in Stormpath for use with our Quickstart guides(https://docs.stormpath.com). It does apply to your subscription's number of reserved applications and can be renamed or reused for your own purposes."", + ""groups"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication1/groups"" + }, + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication1"", + ""loginAttempts"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication1/loginAttempts"" + }, + ""modifiedAt"": ""2015-07-21T23:50:49.622Z"", + ""name"": ""My Application"", + ""oAuthPolicy"": { + ""href"": ""https://api.stormpath.com/v1/oAuthPolicies/foobarApplication1"" + }, + ""passwordResetTokens"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication1/passwordResetTokens"" + }, + ""status"": ""ENABLED"", + ""tenant"": { + ""href"": ""https://api.stormpath.com/v1/tenants/foobarTenant"" + }, + ""verificationEmails"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication1/verificationEmails"" + } + }, + { + ""accountStoreMappings"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2/accountStoreMappings"" + }, + ""accounts"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2/accounts"" + }, + ""apiKeys"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2/apiKeys"" + }, + ""authTokens"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2/authTokens"" + }, + ""createdAt"": ""2015-07-21T23:50:49.079Z"", + ""customData"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2/customData"" + }, + ""defaultAccountStoreMapping"": null, + ""defaultGroupStoreMapping"": null, + ""description"": ""Manages access to the Stormpath Administrator Console and API. This application does not apply to your subscription's number of reserved applications."", + ""groups"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2/groups"" + }, + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2"", + ""loginAttempts"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2/loginAttempts"" + }, + ""modifiedAt"": ""2015-07-21T23:50:49.083Z"", + ""name"": ""Stormpath"", + ""oAuthPolicy"": { + ""href"": ""https://api.stormpath.com/v1/oAuthPolicies/foobarApplication2"" + }, + ""passwordResetTokens"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2/passwordResetTokens"" + }, + ""status"": ""ENABLED"", + ""tenant"": { + ""href"": ""https://api.stormpath.com/v1/tenants/foobarTenant"" + }, + ""verificationEmails"": { + ""href"": ""https://api.stormpath.com/v1/applications/foobarApplication2/verificationEmails"" + } + } + ], + ""limit"": 25, + ""offset"": 0, + ""size"": 2 +}"; + + #endregion } } \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/CollectionTestHarness.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/CollectionTestHarness.cs index bd7f53c0..08bc44f5 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/CollectionTestHarness.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/CollectionTestHarness.cs @@ -29,31 +29,29 @@ public sealed class CollectionTestHarness { internal IDataStore DataStore { get; private set; } - public string Url { get; private set; } - - public string Resource { get; private set; } + public string Href { get; set; } public ICollectionResourceQueryable Queryable { get; private set; } - internal static CollectionTestHarness Create(string url, string resource, IDataStore mockDataStore = null) - where TType : IResource + internal static CollectionTestHarness Create(string collectionHref, IDataStore mockDataStore = null) + where TType : class, IResource { var ds = mockDataStore ?? MockEmptyDataStore(); return new CollectionTestHarness() { DataStore = ds, - Resource = resource, - Url = url, - Queryable = new CollectionResourceQueryable(url, resource, ds) + Href = collectionHref, + Queryable = new CollectionResourceQueryable(collectionHref, ds) }; } private static IDataStore MockEmptyDataStore() + where TType : class, IResource { var emptyMock = Substitute.For(); emptyMock.GetCollectionAsync(Arg.Any()).Returns( - Task.FromResult(new CollectionResponsePageDto() + Task.FromResult(new CollectionResponsePage() { Limit = 0, Offset = 0, diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/LinqAssertExtensions.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/LinqAssertExtensions.cs index ec2cd906..a3cd9473 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/LinqAssertExtensions.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Helpers/LinqAssertExtensions.cs @@ -26,34 +26,35 @@ namespace Stormpath.SDK.Tests.Helpers { public static class LinqAssertExtensions { - public static void GeneratedArgumentsWere(this IQueryable queryable, string url, string resource, string arguments) + public static void GeneratedArgumentsWere(this IQueryable queryable, string href, string arguments) { var resourceQueryable = queryable as ICollectionResourceQueryable; if (resourceQueryable == null) Assertly.Fail("This queryable is not an ICollectionResourceQueryable."); - resourceQueryable.CurrentHref.ShouldBe($"{url}/{resource}?{arguments}"); + resourceQueryable.CurrentHref.ShouldBe($"{href}?{arguments}"); } // The same thing as above, but for testing whether a FakeDataStore received a particular URL call - internal static void WasCalledWithArguments(this IDataStore ds, string url, string resource, string arguments) + internal static void WasCalledWithArguments(this IDataStore ds, string href, string arguments) + where T : class, IResource { var asFake = ds as FakeDataStore; if (asFake != null) { if (string.IsNullOrEmpty(arguments)) - asFake.GetCalls().ShouldContain($"{url}/{resource}"); + asFake.GetCalls().ShouldContain($"{href}"); else - asFake.GetCalls().ShouldContain($"{url}/{resource}?{arguments}"); + asFake.GetCalls().ShouldContain($"{href}?{arguments}"); return; } else { // Maybe it's an NSubstitute mock if (string.IsNullOrEmpty(arguments)) - ds.Received().GetCollectionAsync($"{url}/{resource}"); + ds.Received().GetCollectionAsync($"{href}"); else - ds.Received().GetCollectionAsync($"{url}/{resource}?{arguments}"); + ds.Received().GetCollectionAsync($"{href}?{arguments}"); return; } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/CollectionResourceQueryable_iteration_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/CollectionResourceQueryable_iteration_tests.cs index 74e27aee..f689dc64 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/CollectionResourceQueryable_iteration_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/CollectionResourceQueryable_iteration_tests.cs @@ -27,13 +27,13 @@ namespace Stormpath.SDK.Tests.Impl { public class CollectionResourceQueryable_iteration_tests { - private static string url = "http://f.oo"; - private static string resource = "bar"; + private static string href = "http://f.oo/bar"; [Fact] public async Task First_iteration_does_not_modify_pagination_arguments() { - var harness = CollectionTestHarness.Create(url, resource, + var harness = CollectionTestHarness.Create( + href, new FakeDataStore(FakeAccounts.RebelAlliance)); var query = harness.Queryable @@ -41,25 +41,27 @@ public async Task First_iteration_does_not_modify_pagination_arguments() .Skip(10); await (query as ICollectionResourceQueryable).MoveNextAsync(); - harness.DataStore.WasCalledWithArguments(url, resource, "limit=5&offset=10"); + harness.DataStore.WasCalledWithArguments(href, "limit=5&offset=10"); } [Fact] public async Task First_iteration_does_not_add_pagination_arguments() { - var harness = CollectionTestHarness.Create(url, resource, + var harness = CollectionTestHarness.Create( + href, new FakeDataStore(FakeAccounts.RebelAlliance)); var query = harness.Queryable; await query.MoveNextAsync(); - harness.DataStore.WasCalledWithArguments(url, resource, string.Empty); + harness.DataStore.WasCalledWithArguments(href, string.Empty); } [Fact] public async Task Subsequent_iterations_add_pagination_arguments_if_none_exist() { - var harness = CollectionTestHarness.Create(url, resource, + var harness = CollectionTestHarness.Create( + href, new FakeDataStore(Enumerable.Repeat(new FakeAccount(), 50))); var query = harness.Queryable; @@ -67,7 +69,7 @@ public async Task Subsequent_iterations_add_pagination_arguments_if_none_exist() await query.MoveNextAsync(); var firstPageCount = query.CurrentPage.Count(); await query.MoveNextAsync(); - harness.DataStore.WasCalledWithArguments(url, resource, $"offset={firstPageCount}"); + harness.DataStore.WasCalledWithArguments(href, $"offset={firstPageCount}"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs index 5d3c9a14..bb70c07f 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs @@ -29,6 +29,8 @@ using Stormpath.SDK.Tenant; using Stormpath.SDK.Tests.Fakes; using Xunit; +using Stormpath.SDK.Application; +using Stormpath.SDK.Impl.Application; namespace Stormpath.SDK.Tests.Impl { @@ -102,5 +104,54 @@ public async Task Instantiating_Tenant_from_JSON() (tenant as DefaultTenant).IdSites.Href.ShouldBe("https://api.stormpath.com/v1/tenants/foo-bar/idSites"); (tenant as DefaultTenant).Organizations.Href.ShouldBe("https://api.stormpath.com/v1/tenants/foo-bar/organizations"); } + + [Fact] + public async Task Instantiating_Application_from_JSON() + { + var href = "http://foobar/application"; + fakeRequestExecutor.GetAsync(new Uri(href)) + .Returns(Task.FromResult(FakeJson.Application)); + + var application = await dataStore.GetResourceAsync(href); + + // Verify against data from FakeJson.Application + application.CreatedAt.ShouldBe(Iso8601.Parse("2015-07-21T23:50:49.563Z")); + application.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication"); + application.Description.ShouldBe("This application is so awesome, you don't even know."); + application.ModifiedAt.ShouldBe(Iso8601.Parse("2015-07-21T23:50:49.622Z")); + application.Name.ShouldBe("Lightsabers Galore"); + application.Status.ShouldBe(ApplicationStatus.Enabled); + + (application as DefaultApplication).AccountStoreMappings.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/accountStoreMappings"); + (application as DefaultApplication).Accounts.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/accounts"); + (application as DefaultApplication).ApiKeys.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/apiKeys"); + (application as DefaultApplication).AuthTokens.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/authTokens"); + (application as DefaultApplication).CustomData.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/customData"); + (application as DefaultApplication).DefaultAccountStoreMapping.Href.ShouldBe("https://api.stormpath.com/v1/accountStoreMappings/foobarASM"); + (application as DefaultApplication).DefaultGroupStoreMapping.Href.ShouldBe("https://api.stormpath.com/v1/accountStoreMappings/foobarASM"); + (application as DefaultApplication).Groups.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/groups"); + (application as DefaultApplication).LoginAttempts.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/loginAttempts"); + (application as DefaultApplication).OAuthPolicy.Href.ShouldBe("https://api.stormpath.com/v1/oAuthPolicies/foobarApplication"); + (application as DefaultApplication).PasswordResetToken.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/passwordResetTokens"); + (application as DefaultApplication).Tenant.Href.ShouldBe("https://api.stormpath.com/v1/tenants/foobarTenant"); + (application as DefaultApplication).VerificationEmails.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/verificationEmails"); + + } + + [Fact] + public async Task Instantiating_application_list_from_JSON() + { + var href = "http://foobar/applications"; + fakeRequestExecutor.GetAsync(new Uri(href)) + .Returns(Task.FromResult(FakeJson.ApplicationList)); + + var applicationList = await dataStore.GetCollectionAsync(href); + + applicationList.Href.ShouldBe("https://api.stormpath.com/v1/tenants/foobarTenant/applications"); + applicationList.Size.ShouldBe(2); + applicationList.Offset.ShouldBe(0); + applicationList.Limit.ShouldBe(25); + applicationList.Items.Count.ShouldBe(2); + } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Expand_extension_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Expand_extension_tests.cs index 8de7816d..ad2b3796 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Expand_extension_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Expand_extension_tests.cs @@ -30,7 +30,7 @@ public void Expand_one_link() var query = Harness.Queryable .Expand(x => x.GetDirectoryAsync()); - query.GeneratedArgumentsWere(Url, Resource, "expand=directory"); + query.GeneratedArgumentsWere(Href, "expand=directory"); } [Fact] @@ -40,7 +40,7 @@ public void Expand_multiple_links() .Expand(x => x.GetDirectoryAsync()) .Expand(x => x.GetTenantAsync()); - query.GeneratedArgumentsWere(Url, Resource, "expand=directory,tenant"); + query.GeneratedArgumentsWere(Href, "expand=directory,tenant"); } [Fact] @@ -49,7 +49,7 @@ public void Expand_collection_query_with_offset() var query = Harness.Queryable .Expand(x => x.GetGroupsAsync(), offset: 10); - query.GeneratedArgumentsWere(Url, Resource, "expand=groups(offset:10)"); + query.GeneratedArgumentsWere(Href, "expand=groups(offset:10)"); } [Fact] @@ -58,7 +58,7 @@ public void Expand_collection_query_with_limit() var query = Harness.Queryable .Expand(x => x.GetGroupsAsync(), limit: 20); - query.GeneratedArgumentsWere(Url, Resource, "expand=groups(limit:20)"); + query.GeneratedArgumentsWere(Href, "expand=groups(limit:20)"); } [Fact] @@ -67,7 +67,7 @@ public void Expand_collection_query_with_both_parameters() var query = Harness.Queryable .Expand(x => x.GetGroupsAsync(), 5, 15); - query.GeneratedArgumentsWere(Url, Resource, "expand=groups(offset:5,limit:15)"); + query.GeneratedArgumentsWere(Href, "expand=groups(offset:5,limit:15)"); } [Fact] @@ -78,7 +78,7 @@ public void Expand_all_the_things() .Expand(x => x.GetGroupsAsync(), 10, 20) .Expand(x => x.GetDirectoryAsync()); - query.GeneratedArgumentsWere(Url, Resource, "expand=tenant,groups(offset:10,limit:20),directory"); + query.GeneratedArgumentsWere(Href, "expand=tenant,groups(offset:10,limit:20),directory"); } [Fact] @@ -87,7 +87,7 @@ public void Expand_throws_if_used_on_an_attribute() Should.Throw(() => { var query = Harness.Queryable.Expand(x => x.Email); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -97,7 +97,7 @@ public void Expand_throws_if_parameters_are_supplied_for_link() Should.Throw(() => { var query = Harness.Queryable.Expand(x => x.GetDirectoryAsync(), limit: 10); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -107,7 +107,7 @@ public void Expand_throws_if_syntax_is_dumb() Should.Throw(() => { var query = Harness.Queryable.Expand(x => x.GetTenantAsync().GetAwaiter()); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Filter_extension_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Filter_extension_tests.cs index 661bf66e..e85329f5 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Filter_extension_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Filter_extension_tests.cs @@ -30,7 +30,7 @@ public void Filter_with_simple_parameter() .Filter("Joe"); // Assert - query.GeneratedArgumentsWere(Url, Resource, "q=Joe"); + query.GeneratedArgumentsWere(Href, "q=Joe"); } [Fact] @@ -41,7 +41,7 @@ public void Filter_multiple_calls_are_LIFO() .Filter("Joey"); // Expected behavior: the last call will be kept - query.GeneratedArgumentsWere(Url, Resource, "q=Joey"); + query.GeneratedArgumentsWere(Href, "q=Joey"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FirstAsync_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FirstAsync_tests.cs index c5dd894d..2eb83b19 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FirstAsync_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/FirstAsync_tests.cs @@ -37,7 +37,7 @@ public async Task FirstAsync_returns_first() FakeAccounts.LukeSkywalker, FakeAccounts.HanSolo }); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var luke = await harness.Queryable.FirstAsync(); @@ -48,7 +48,7 @@ public async Task FirstAsync_returns_first() public void FirstAsync_throws_when_no_items_exist() { var fakeDataStore = new FakeDataStore(Enumerable.Empty()); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); // TODO InvalidOperationException (after xUnit 2.0) Should.Throw(async () => @@ -65,7 +65,7 @@ public async Task FirstOrDefaultAsync_returns_first() FakeAccounts.LukeSkywalker, FakeAccounts.HanSolo }); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var luke = await harness.Queryable.FirstOrDefaultAsync(); @@ -76,7 +76,7 @@ public async Task FirstOrDefaultAsync_returns_first() public async Task FirstOrDefaultAsync_returns_null_when_no_items_exist() { var fakeDataStore = new FakeDataStore(Enumerable.Empty()); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var notLuke = await harness.Queryable.FirstOrDefaultAsync(); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Linq_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Linq_tests.cs index f115f644..0db35330 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Linq_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Linq_tests.cs @@ -23,29 +23,23 @@ namespace Stormpath.SDK.Tests.Impl.Linq { public class Linq_tests { - private readonly string url = "http://f.oo"; - private readonly string resource = "bar"; + private static string href = "http://f.oo/bar"; private CollectionTestHarness harness; public Linq_tests() { // Default test harness. Child classes can overwrite - harness = CollectionTestHarness.Create(Url, Resource); + harness = CollectionTestHarness.Create(Href); } internal Linq_tests(IDataStore ds) { - harness = CollectionTestHarness.Create(Url, Resource, ds); + harness = CollectionTestHarness.Create(Href, ds); } - protected string Url + protected string Href { - get { return url; } - } - - protected string Resource - { - get { return resource; } + get { return href; } } protected CollectionTestHarness Harness diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderBy_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderBy_tests.cs index f563f053..2cab181a 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderBy_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/OrderBy_tests.cs @@ -33,7 +33,7 @@ public void Order_by_a_field() var query = Harness.Queryable .OrderBy(x => x.GivenName); - query.GeneratedArgumentsWere(Url, Resource, "orderBy=givenName"); + query.GeneratedArgumentsWere(Href, "orderBy=givenName"); } [Fact] @@ -42,7 +42,7 @@ public void Order_by_a_field_descending() var query = Harness.Queryable .OrderByDescending(x => x.Email); - query.GeneratedArgumentsWere(Url, Resource, "orderBy=email desc"); + query.GeneratedArgumentsWere(Href, "orderBy=email desc"); } [Fact] @@ -52,7 +52,7 @@ public void Order_by_multiple_fields() .OrderBy(x => x.GivenName) .ThenByDescending(x => x.Username); - query.GeneratedArgumentsWere(Url, Resource, "orderBy=givenName,username desc"); + query.GeneratedArgumentsWere(Href, "orderBy=givenName,username desc"); } [Fact] diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Result_operators_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Result_operators_tests.cs index 386b1a04..3ca86df2 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Result_operators_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Result_operators_tests.cs @@ -40,7 +40,7 @@ public void First_generates_proper_arguments() var firstRebel = Harness.Queryable .First(); - Harness.DataStore.WasCalledWithArguments(Url, Resource, "limit=1"); + Harness.DataStore.WasCalledWithArguments(Href, "limit=1"); firstRebel.ShouldBe(FakeAccounts.RebelAlliance.First()); } @@ -48,21 +48,22 @@ public void First_generates_proper_arguments() public void FirstOrDefault_generates_proper_arguments() { // (Empty data store) - Harness = CollectionTestHarness.Create(Url, Resource); + Harness = CollectionTestHarness.Create(Href); // Execution behavior: // Limit the query to 1 result so we can minimize transfer over the wire var firstRebel = Harness.Queryable .FirstOrDefault(); - Harness.DataStore.WasCalledWithArguments(Url, Resource, "limit=1"); + Harness.DataStore.WasCalledWithArguments(Href, "limit=1"); firstRebel.ShouldBe(null); } [Fact] public void Single_generates_proper_arguments() { - Harness = CollectionTestHarness.Create(Url, Resource, + Harness = CollectionTestHarness.Create( + Href, new FakeDataStore(new List() { FakeAccounts.BobaFett })); // Execution behavior: @@ -70,7 +71,7 @@ public void Single_generates_proper_arguments() var boba = Harness.Queryable .Single(); - Harness.DataStore.WasCalledWithArguments(Url, Resource, "limit=1"); + Harness.DataStore.WasCalledWithArguments(Href, "limit=1"); boba.ShouldBe(FakeAccounts.BobaFett); } @@ -78,14 +79,14 @@ public void Single_generates_proper_arguments() public void SingleOrDefault_generates_proper_arguments() { // (Empty data store) - Harness = CollectionTestHarness.Create(Url, Resource); + Harness = CollectionTestHarness.Create(Href); // Execution behavior: // Limit the query to 1 result so we can minimize transfer over the wire var boba = Harness.Queryable .SingleOrDefault(); - Harness.DataStore.WasCalledWithArguments(Url, Resource, "limit=1"); + Harness.DataStore.WasCalledWithArguments(Href, "limit=1"); boba.ShouldBe(null); } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/SingleAsync_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/SingleAsync_tests.cs index d804d17d..670532d9 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/SingleAsync_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/SingleAsync_tests.cs @@ -36,7 +36,7 @@ public async Task SingleAsync_returns_one() { FakeAccounts.HanSolo }); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var han = await harness.Queryable.SingleAsync(); @@ -51,7 +51,7 @@ public void SingleAsync_throws_when_more_than_one_item_exists() FakeAccounts.HanSolo, FakeAccounts.LukeSkywalker }); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); // TODO InvalidOperationException (after xUnit 2.0) Should.Throw(async () => @@ -64,7 +64,7 @@ public void SingleAsync_throws_when_more_than_one_item_exists() public void SingleAsync_throws_when_no_items_exist() { var fakeDataStore = new FakeDataStore(Enumerable.Empty()); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); // TODO InvalidOperationException (after xUnit 2.0) Should.Throw(async () => @@ -80,7 +80,7 @@ public async Task SingleOrDefaultAsync_returns_one() { FakeAccounts.HanSolo }); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var han = await harness.Queryable.SingleOrDefaultAsync(); @@ -95,7 +95,7 @@ public void SingleOrDefaultAsync_throws_when_more_than_one_item_exists() FakeAccounts.HanSolo, FakeAccounts.LukeSkywalker }); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); // TODO InvalidOperationException (after xUnit 2.0) Should.Throw(async () => @@ -108,7 +108,7 @@ public void SingleOrDefaultAsync_throws_when_more_than_one_item_exists() public async Task SingleOrDefaultAsync_returns_null_when_no_items_exist() { var fakeDataStore = new FakeDataStore(Enumerable.Empty()); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var notHan = await harness.Queryable.SingleOrDefaultAsync(); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Skip_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Skip_tests.cs index 8518054d..b9440943 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Skip_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Skip_tests.cs @@ -30,7 +30,7 @@ public void Skip_becomes_offset() var query = Harness.Queryable .Skip(10); - query.GeneratedArgumentsWere(Url, Resource, "offset=10"); + query.GeneratedArgumentsWere(Href, "offset=10"); } [Fact] @@ -40,7 +40,7 @@ public void Skip_with_variable_becomes_offset() var query = Harness.Queryable .Skip(offset); - query.GeneratedArgumentsWere(Url, Resource, "offset=20"); + query.GeneratedArgumentsWere(Href, "offset=20"); } [Fact] @@ -50,7 +50,7 @@ public void Skip_with_function_becomes_offset() var query = Harness.Queryable .Skip(offsetFunc()); - query.GeneratedArgumentsWere(Url, Resource, "offset=25"); + query.GeneratedArgumentsWere(Href, "offset=25"); } [Fact] @@ -60,7 +60,7 @@ public void Skip_multiple_calls_are_LIFO() .Skip(10).Skip(5); // Expected behavior: the last call will be kept - query.GeneratedArgumentsWere(Url, Resource, "offset=5"); + query.GeneratedArgumentsWere(Href, "offset=5"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/SyncAdapter_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/SyncAdapter_tests.cs index 49740183..3b57f380 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/SyncAdapter_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/SyncAdapter_tests.cs @@ -29,7 +29,8 @@ public class SyncAdapter_tests : Linq_tests [Fact] public void ToList_synchronously_iterates_thru_collection() { - var harness = CollectionTestHarness.Create(Url, Resource, + var harness = CollectionTestHarness.Create( + Href, new FakeDataStore(Enumerable.Repeat(FakeAccounts.DarthVader, 52))); var items = harness.Queryable @@ -42,7 +43,8 @@ public void ToList_synchronously_iterates_thru_collection() [Fact] public void Take_limit_is_observed() { - var harness = CollectionTestHarness.Create(Url, Resource, + var harness = CollectionTestHarness.Create( + Href, new FakeDataStore(Enumerable.Repeat(FakeAccounts.DarthVader, 52))); var items = harness.Queryable diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Take_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Take_tests.cs index a691219b..9abe0c61 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Take_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Take_tests.cs @@ -30,7 +30,7 @@ public void Take_with_constant_becomes_limit() var query = Harness.Queryable .Take(10); - query.GeneratedArgumentsWere(Url, Resource, "limit=10"); + query.GeneratedArgumentsWere(Href, "limit=10"); } [Fact] @@ -40,7 +40,7 @@ public void Take_with_variable_becomes_limit() var query = Harness.Queryable .Take(limit); - query.GeneratedArgumentsWere(Url, Resource, "limit=20"); + query.GeneratedArgumentsWere(Href, "limit=20"); } [Fact] @@ -50,7 +50,7 @@ public void Take_with_function_becomes_limit() var query = Harness.Queryable .Take(limitFunc()); - query.GeneratedArgumentsWere(Url, Resource, "limit=25"); + query.GeneratedArgumentsWere(Href, "limit=25"); } [Fact] @@ -60,7 +60,7 @@ public void Take_multiple_calls_are_LIFO() .Take(10).Take(5); // Expected behavior: the last call will be kept - query.GeneratedArgumentsWere(Url, Resource, "limit=5"); + query.GeneratedArgumentsWere(Href, "limit=5"); } [Fact] @@ -70,7 +70,7 @@ public void Take_limit_is_100() .Take(101); // Expected behavior: the last call will be kept - query.GeneratedArgumentsWere(Url, Resource, "limit=100"); + query.GeneratedArgumentsWere(Href, "limit=100"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ToListAsync_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ToListAsync_tests.cs index 0d1fcd27..fbe6c58a 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ToListAsync_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/ToListAsync_tests.cs @@ -34,7 +34,7 @@ public class ToListAsync_tests : Linq_tests public async Task ToListAsync_returns_empty_list_for_no_items() { var fakeDataStore = new FakeDataStore(Enumerable.Empty()); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var empty = await harness.Queryable.ToListAsync(); @@ -45,7 +45,7 @@ public async Task ToListAsync_returns_empty_list_for_no_items() public async Task ToListAsync_retrieves_all_items() { var fakeDataStore = new FakeDataStore(FakeAccounts.RebelAlliance); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var alliance = await harness.Queryable.ToListAsync(); @@ -60,7 +60,7 @@ public async Task ToListAsync_checks_for_new_items_after_last_page() // will make another call to the server, just to make sure another item hasn't been added // to the end while we were enumerating. var fakeDataStore = new FakeDataStore(Enumerable.Repeat(new FakeAccount(), 51)); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var longList = await harness.Queryable.ToListAsync(); @@ -75,7 +75,7 @@ public async Task ToListAsync_observes_take_limit() // in Stormpath, even though that's what it translates to. .Take() represents an // upper limit to the items that are returned. Take(5) returns 5 items, Take(500) returns 500. var fakeDataStore = new FakeDataStore(Enumerable.Repeat(new FakeAccount(), 51)); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var longList = await harness.Queryable .Take(7) @@ -92,7 +92,7 @@ public async Task ToListAsync_pages_until_take_limit_is_reached() // in Stormpath, even though that's what it translates to. .Take() represents an // upper limit to the items that are returned. Take(5) returns 5 items, Take(500) returns 500. var fakeDataStore = new FakeDataStore(Enumerable.Repeat(new FakeAccount(), 750)); - var harness = CollectionTestHarness.Create(Url, Resource, fakeDataStore); + var harness = CollectionTestHarness.Create(Href, fakeDataStore); var longList = await harness.Queryable .Take(500) @@ -105,7 +105,8 @@ public async Task ToListAsync_pages_until_take_limit_is_reached() [Fact] public async Task ForEachAsync_operates_on_every_item() { - var harness = CollectionTestHarness.Create(Url, Resource, + var harness = CollectionTestHarness.Create( + Href, new FakeDataStore(FakeAccounts.RebelAlliance)); var gmailAlliance = new List(); @@ -120,7 +121,8 @@ await harness.Queryable.ForEachAsync(acct => [Fact] public async Task ForEachAsync_indexes_every_item() { - var harness = CollectionTestHarness.Create(Url, Resource, + var harness = CollectionTestHarness.Create( + Href, new FakeDataStore(FakeAccounts.GalacticEmpire)); var empireFirstNameLookup = new Dictionary(); @@ -135,7 +137,8 @@ await harness.Queryable.ForEachAsync((acct, index) => [Fact] public async Task ForEachAsync_can_be_cancelled() { - var harness = CollectionTestHarness.Create(Url, Resource, + var harness = CollectionTestHarness.Create( + Href, new FakeDataStore(FakeAccounts.GalacticEmpire)); var cts = new CancellationTokenSource(); var reachedIndex = -1; diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Unsupported_filters_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Unsupported_filters_tests.cs index b71879de..40da0c7e 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Unsupported_filters_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Unsupported_filters_tests.cs @@ -60,7 +60,7 @@ public void Cast_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.Cast(); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -70,7 +70,7 @@ public void Concat_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.Concat(Enumerable.Empty()); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -89,7 +89,7 @@ public void Distinct_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.Distinct(); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -99,7 +99,7 @@ public void Except_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.Except(Enumerable.Empty()); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -109,7 +109,7 @@ public void GroupBy_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.GroupBy(x => x.Email); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -123,7 +123,7 @@ public void GroupJoin_clause_is_unsupported() outer => outer.Email, inner => inner.Username, (outer, results) => new { outer.CreatedAt, results }); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -133,7 +133,7 @@ public void Intersect_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.Intersect(Enumerable.Empty()); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -147,7 +147,7 @@ public void Join_clause_is_unsupported() outer => outer.Email, inner => inner.Username, (outer, inner) => outer.CreatedAt); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -189,7 +189,7 @@ public void OfType_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.OfType(); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -199,7 +199,7 @@ public void Reverse_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.Reverse(); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -210,7 +210,7 @@ public void Select_is_unsupported() { var query = Harness.Queryable .Select(x => x.Email); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -220,7 +220,7 @@ public void SelectMany_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.SelectMany(x => x.Email); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -239,7 +239,7 @@ public void SkipWhile_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.SkipWhile(x => x.Email == "foobar"); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -258,7 +258,7 @@ public void TakeWhile_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.TakeWhile(x => x.Email == "foobar"); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -268,7 +268,7 @@ public void Union_is_unsupported() Should.Throw(() => { var query = Harness.Queryable.Union(Enumerable.Empty()); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -280,7 +280,7 @@ public void Zip_is_unsupported() var query = Harness.Queryable.Zip( Enumerable.Empty(), (first, second) => first.Email == second.Email); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Where_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Where_tests.cs index e8167f7d..16e69624 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Where_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Where_tests.cs @@ -31,13 +31,13 @@ public void Where_throws_for_constant() Should.Throw(() => { var query = Harness.Queryable.Where(x => true); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); Should.Throw(() => { var query = Harness.Queryable.Where(x => false); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -47,7 +47,7 @@ public void Where_throws_for_unsupported_comparison_operators() Should.Throw(() => { var query = Harness.Queryable.Where(x => x.Email != "foo"); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -57,13 +57,13 @@ public void Where_throws_for_more_complex_overloads_of_helper_methods() Should.Throw(() => { var query = Harness.Queryable.Where(x => x.Email.Equals("bar", StringComparison.CurrentCulture)); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); Should.Throw(() => { var query = Harness.Queryable.Where(x => x.Email.StartsWith("foo", StringComparison.OrdinalIgnoreCase)); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -73,7 +73,7 @@ public void Where_throws_for_unsupported_helper_methods() Should.Throw(() => { var query = Harness.Queryable.Where(x => x.Email.ToUpper() == "FOO"); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -83,7 +83,7 @@ public void Where_throws_for_binary_or() Should.Throw(() => { var query = Harness.Queryable.Where(x => x.Email == "foo" || x.Email == "bar"); - query.GeneratedArgumentsWere(Url, Resource, ""); + query.GeneratedArgumentsWere(Href, ""); }); } @@ -93,7 +93,7 @@ public void Where_attribute_equals() var query = Harness.Queryable .Where(x => x.Email == "tk421@deathstar.co"); - query.GeneratedArgumentsWere(Url, Resource, "email=tk421@deathstar.co"); + query.GeneratedArgumentsWere(Href, "email=tk421@deathstar.co"); } [Fact] @@ -102,7 +102,7 @@ public void Where_attribute_equals_using_helper_method() var query = Harness.Queryable .Where(x => x.Email.Equals("tk421@deathstar.co")); - query.GeneratedArgumentsWere(Url, Resource, "email=tk421@deathstar.co"); + query.GeneratedArgumentsWere(Href, "email=tk421@deathstar.co"); } [Fact] @@ -111,7 +111,7 @@ public void Where_attribute_starts_with() var query = Harness.Queryable .Where(x => x.Email.StartsWith("tk421")); - query.GeneratedArgumentsWere(Url, Resource, "email=tk421*"); + query.GeneratedArgumentsWere(Href, "email=tk421*"); } [Fact] @@ -120,7 +120,7 @@ public void Where_attribute_ends_with() var query = Harness.Queryable .Where(x => x.Email.EndsWith("deathstar.co")); - query.GeneratedArgumentsWere(Url, Resource, "email=*deathstar.co"); + query.GeneratedArgumentsWere(Href, "email=*deathstar.co"); } [Fact] @@ -129,7 +129,7 @@ public void Where_attribute_contains() var query = Harness.Queryable .Where(x => x.Email.Contains("421")); - query.GeneratedArgumentsWere(Url, Resource, "email=*421*"); + query.GeneratedArgumentsWere(Href, "email=*421*"); } [Fact] @@ -138,7 +138,7 @@ public void Where_multiple_attributes_with_and() var query = Harness.Queryable .Where(x => x.Email == "tk421@deathstar.co" && x.Username == "tk421"); - query.GeneratedArgumentsWere(Url, Resource, "email=tk421@deathstar.co&username=tk421"); + query.GeneratedArgumentsWere(Href, "email=tk421@deathstar.co&username=tk421"); } [Fact] @@ -148,7 +148,7 @@ public void Where_multiple_wheres() .Where(x => x.Email == "tk421@deathstar.co") .Where(x => x.Username.StartsWith("tk421")); - query.GeneratedArgumentsWere(Url, Resource, "email=tk421@deathstar.co&username=tk421*"); + query.GeneratedArgumentsWere(Href, "email=tk421@deathstar.co&username=tk421*"); } [Fact] @@ -158,7 +158,7 @@ public void Where_date_attribute_greater_than() var query = Harness.Queryable .Where(x => x.CreatedAt > testDate); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=(2015-01-01T06:00:00Z,]"); + query.GeneratedArgumentsWere(Href, "createdAt=(2015-01-01T06:00:00Z,]"); } [Fact] @@ -168,7 +168,7 @@ public void Where_date_attribute_greater_than_or_equalto() var query = Harness.Queryable .Where(x => x.CreatedAt >= testDate); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=[2015-01-01T06:00:00Z,]"); + query.GeneratedArgumentsWere(Href, "createdAt=[2015-01-01T06:00:00Z,]"); } [Fact] @@ -178,7 +178,7 @@ public void Where_date_attribute_less_than() var query = Harness.Queryable .Where(x => x.ModifiedAt < testDate); - query.GeneratedArgumentsWere(Url, Resource, "modifiedAt=[,2016-01-01T12:00:00Z)"); + query.GeneratedArgumentsWere(Href, "modifiedAt=[,2016-01-01T12:00:00Z)"); } [Fact] @@ -188,7 +188,7 @@ public void Where_date_attribute_less_than_or_equalto() var query = Harness.Queryable .Where(x => x.ModifiedAt <= testDate); - query.GeneratedArgumentsWere(Url, Resource, "modifiedAt=[,2016-01-01T12:00:00Z]"); + query.GeneratedArgumentsWere(Href, "modifiedAt=[,2016-01-01T12:00:00Z]"); } [Fact] @@ -199,7 +199,7 @@ public void Where_date_attribute_between() var query = Harness.Queryable .Where(x => x.CreatedAt > testStartDate && x.CreatedAt <= testEndDate); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=(2015-01-01T00:00:00Z,2015-12-31T23:59:59Z]"); + query.GeneratedArgumentsWere(Href, "createdAt=(2015-01-01T00:00:00Z,2015-12-31T23:59:59Z]"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Within_extension_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Within_extension_tests.cs index bc190daa..57249b8a 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Within_extension_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Linq/Within_extension_tests.cs @@ -41,7 +41,7 @@ public void Where_date_using_shorthand_for_year() var query = Harness.Queryable .Where(x => x.CreatedAt.Within(2015)); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=2015"); + query.GeneratedArgumentsWere(Href, "createdAt=2015"); } [Fact] @@ -50,7 +50,7 @@ public void Where_date_using_shorthand_for_month() var query = Harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01)); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=2015-01"); + query.GeneratedArgumentsWere(Href, "createdAt=2015-01"); } [Fact] @@ -59,7 +59,7 @@ public void Where_date_using_shorthand_for_day() var query = Harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01, 01)); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=2015-01-01"); + query.GeneratedArgumentsWere(Href, "createdAt=2015-01-01"); } [Fact] @@ -68,7 +68,7 @@ public void Where_date_using_shorthand_for_hour() var query = Harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01, 01, 12)); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=2015-01-01T12"); + query.GeneratedArgumentsWere(Href, "createdAt=2015-01-01T12"); } [Fact] @@ -77,7 +77,7 @@ public void Where_date_using_shorthand_for_minute() var query = Harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30)); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=2015-01-01T12:30"); + query.GeneratedArgumentsWere(Href, "createdAt=2015-01-01T12:30"); } [Fact] @@ -86,7 +86,7 @@ public void Where_date_using_shorthand_for_second() var query = Harness.Queryable .Where(x => x.CreatedAt.Within(2015, 01, 01, 12, 30, 31)); - query.GeneratedArgumentsWere(Url, Resource, "createdAt=2015-01-01T12:30:31"); + query.GeneratedArgumentsWere(Href, "createdAt=2015-01-01T12:30:31"); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Account/AccountStatus.cs b/Stormpath.SDK/Stormpath.SDK/Account/AccountStatus.cs index 3920d3b3..b14c024e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Account/AccountStatus.cs +++ b/Stormpath.SDK/Stormpath.SDK/Account/AccountStatus.cs @@ -55,11 +55,6 @@ private AccountStatus(int value, string displayName) { } - public static explicit operator AccountStatus(string status) - { - throw new NotImplementedException(); - } - public static AccountStatus Parse(string status) { AccountStatus found; diff --git a/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs b/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs index 799d8814..2f1a8564 100644 --- a/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs +++ b/Stormpath.SDK/Stormpath.SDK/Application/ApplicationStatus.cs @@ -15,18 +15,47 @@ // limitations under the License. // +using System; +using System.Collections.Generic; +using Stormpath.SDK.Shared; + namespace Stormpath.SDK.Application { - public enum ApplicationStatus + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields must be private", Justification = "Public enumeration elements")] + public sealed class ApplicationStatus : Enumeration { /// - /// Accounts are prevented from logging into this application. + /// Accounts can log into this application. /// - Disabled = 0, + public static ApplicationStatus Enabled = new ApplicationStatus(0, "ENABLED"); /// - /// Accounts are allowed to log into this application. + /// Accounts are prevented from logging into this application. /// - Enabled = 1 + public static ApplicationStatus Disabled = new ApplicationStatus(1, "DISABLED"); + + private static readonly Dictionary LookupMap = new Dictionary() + { + { Enabled.DisplayName, Enabled }, + { Disabled.DisplayName, Disabled }, + }; + + private ApplicationStatus() + { + } + + private ApplicationStatus(int value, string displayName) + : base(value, displayName) + { + } + + public static ApplicationStatus Parse(string status) + { + ApplicationStatus found; + if (!LookupMap.TryGetValue(status.ToUpper(), out found)) + throw new ApplicationException($"Could not parse status value '{status.ToUpper()}'"); + + return found; + } } -} +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Application/IApplication.cs b/Stormpath.SDK/Stormpath.SDK/Application/IApplication.cs index f5682c59..5e9f5877 100644 --- a/Stormpath.SDK/Stormpath.SDK/Application/IApplication.cs +++ b/Stormpath.SDK/Stormpath.SDK/Application/IApplication.cs @@ -19,10 +19,12 @@ namespace Stormpath.SDK.Application { - public interface IApplication : IResource + public interface IApplication : IResource, ISaveable, IExtendable, IAuditable { string Name { get; } + string Description { get; } + ApplicationStatus Status { get; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.cs new file mode 100644 index 00000000..7a67092d --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Stormpath.SDK.Application; +using Stormpath.SDK.Impl.DataStore; +using Stormpath.SDK.Impl.Resource; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stormpath.SDK.Impl.Application +{ + internal sealed class DefaultApplication : AbstractExtendableInstanceResource, IApplication + { + private static readonly string AccountStoreMappingsPropertyName = "accountStoreMappings"; + private static readonly string AccountsPropertyName = "accounts"; + private static readonly string ApiKeysPropertyName = "apiKeys"; + private static readonly string AuthTokensPropertyName = "authTokens"; + private static readonly string DefaultAccountStoreMappingPropertyName = "defaultAccountStoreMapping"; + private static readonly string DefaultGroupStoreMappingPropertyName = "defaultGroupStoreMapping"; + private static readonly string DescriptionPropertyName = "description"; + private static readonly string GroupsPropertyName = "groups"; + private static readonly string LoginAttemptsPropertyName = "loginAttempts"; + private static readonly string NamePropertyName = "name"; + private static readonly string OAuthPolicyPropertyName = "oAuthPolicy"; + private static readonly string PasswordResetTokensPropertyName = "passwordResetTokens"; + private static readonly string StatusPropertyName = "status"; + private static readonly string TenantPropertyName = "tenant"; + private static readonly string VerificationEmailsPropertyName = "verificationEmails"; + + public DefaultApplication(IDataStore dataStore) + : base(dataStore) + { + } + + public DefaultApplication(IDataStore dataStore, Hashtable properties) + : base(dataStore, properties) + { + } + + internal LinkProperty AccountStoreMappings => GetLinkProperty(AccountStoreMappingsPropertyName); + + internal LinkProperty Accounts => GetLinkProperty(AccountsPropertyName); + + internal LinkProperty ApiKeys => GetLinkProperty(ApiKeysPropertyName); + + internal LinkProperty AuthTokens => GetLinkProperty(AuthTokensPropertyName); + + internal LinkProperty DefaultAccountStoreMapping => GetLinkProperty(DefaultAccountStoreMappingPropertyName); + + internal LinkProperty DefaultGroupStoreMapping => GetLinkProperty(DefaultGroupStoreMappingPropertyName); + + string IApplication.Description => GetProperty(DescriptionPropertyName); + + internal LinkProperty Groups => GetLinkProperty(GroupsPropertyName); + + string IApplication.Name => GetProperty(NamePropertyName); + + internal LinkProperty LoginAttempts => GetLinkProperty(LoginAttemptsPropertyName); + + internal LinkProperty OAuthPolicy => GetLinkProperty(OAuthPolicyPropertyName); + + internal LinkProperty PasswordResetToken => GetLinkProperty(PasswordResetTokensPropertyName); + + ApplicationStatus IApplication.Status => GetProperty(StatusPropertyName); + + internal LinkProperty Tenant => GetLinkProperty(TenantPropertyName); + + internal LinkProperty VerificationEmails => GetLinkProperty(VerificationEmailsPropertyName); + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs index 44b0a4d0..b963bf3c 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs @@ -23,6 +23,7 @@ using Stormpath.SDK.Client; using Stormpath.SDK.Impl.DataStore; using Stormpath.SDK.Impl.Extensions; +using Stormpath.SDK.Resource; using Stormpath.SDK.Tenant; namespace Stormpath.SDK.Impl.Client @@ -69,7 +70,7 @@ Task ITenantActions.CreateApplicationAsync(IApplication applicatio throw new NotImplementedException(); } - IApplicationAsyncList ITenantActions.GetApplications() + ICollectionResourceQueryable ITenantActions.GetApplications() { // return new CollectionResourceQueryable() throw new NotImplementedException(); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceFactory.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceFactory.cs index 665988be..4e4ff3e1 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceFactory.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceFactory.cs @@ -18,10 +18,14 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using Stormpath.SDK.Account; using Stormpath.SDK.Impl.Account; using Stormpath.SDK.Impl.Tenant; using Stormpath.SDK.Tenant; +using Stormpath.SDK.Impl.Resource; +using Stormpath.SDK.Application; +using Stormpath.SDK.Impl.Application; namespace Stormpath.SDK.Impl.DataStore { @@ -30,7 +34,8 @@ internal sealed class DefaultResourceFactory : IResourceFactory private readonly Dictionary typeMap = new Dictionary() { { typeof(IAccount), typeof(DefaultAccount) }, - { typeof(ITenant), typeof(DefaultTenant) } + { typeof(IApplication), typeof(DefaultApplication) }, + { typeof(ITenant), typeof(DefaultTenant) }, }; private readonly IDataStore dataStore; @@ -41,10 +46,7 @@ public DefaultResourceFactory(IDataStore dataStore) } [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element must begin with upper-case letter", Justification = "Reviewed")] - private IResourceFactory _this - { - get { return this; } - } + private IResourceFactory _this => this; T IResourceFactory.Instantiate() { @@ -52,10 +54,25 @@ T IResourceFactory.Instantiate() } T IResourceFactory.Instantiate(Hashtable properties) + { + bool isCollection = typeof(T).IsGenericType + && typeof(T).GetGenericTypeDefinition() == typeof(CollectionResponsePage<>); + if (isCollection) + return InstantiateCollection(properties); + + return InstantiateSingle(properties); + } + + private T InstantiateSingle(Hashtable properties) + { + return (T)InstantiateSingle(properties, typeof(T)); + } + + private object InstantiateSingle(Hashtable properties, Type type) { Type targetType; - if (!typeMap.TryGetValue(typeof(T), out targetType)) - throw new ApplicationException($"Unknown resource type {typeof(T).Name}"); + if (!typeMap.TryGetValue(type, out targetType)) + throw new ApplicationException($"Unknown resource type {type.Name}"); object targetObject; try @@ -70,11 +87,57 @@ T IResourceFactory.Instantiate(Hashtable properties) throw new ApplicationException($"Error creating resource type {targetType.Name}", e); } - var targetObjectAsT = targetObject as T; - if (targetObjectAsT == null) - throw new ApplicationException($"Unable to create resource type {targetType.Name}"); + return targetObject; + } + + private T InstantiateCollection(Hashtable properties) + { + var outerType = typeof(T); // CollectionResponsePage + + Type innerType = outerType.GetGenericArguments().SingleOrDefault(); + if (innerType == null || !typeMap.ContainsKey(innerType)) + throw new ApplicationException($"Error creating collection resource: unknown inner type {outerType.GetGenericArguments().SingleOrDefault()?.Name}."); - return targetObjectAsT; + if (properties == null) + throw new ApplicationException($"Unable to create collection resource of type {innerType.Name}: no properties to materialize with."); + + int offset, limit, size; + if (!int.TryParse(properties["offset"]?.ToString(), out offset)) + throw new ApplicationException($"Unable to create collection resource of type {innerType.Name}: invalid 'offset' value."); + if (!int.TryParse(properties["limit"]?.ToString(), out limit)) + throw new ApplicationException($"Unable to create collection resource of type {innerType.Name}: invalid 'limit' value."); + if (!int.TryParse(properties["size"]?.ToString(), out size)) + throw new ApplicationException($"Unable to create collection resource of type {innerType.Name}: invalid 'size' value."); + + var href = properties["href"]?.ToString(); + if (string.IsNullOrEmpty(href)) + throw new ApplicationException($"Unable to create collection resource of type {innerType.Name}: invalid 'href' value."); + + var items = properties["items"] as List; + if (items == null) + throw new ApplicationException($"Unable to create collection resource of type {innerType.Name}: items subcollection is invalid."); + + try + { + Type listOfInnerType = typeof(List<>).MakeGenericType(innerType); + var materializedItems = listOfInnerType.GetConstructor(Type.EmptyTypes).Invoke(Type.EmptyTypes); + var addMethod = listOfInnerType.GetMethod("Add", new Type[] { innerType }); + + foreach (var item in items) + { + var materialized = InstantiateSingle(item, innerType); + addMethod.Invoke(materializedItems, new object[] { materialized }); + } + + object targetObject; + targetObject = Activator.CreateInstance(outerType, new object[] { href, offset, limit, size, materializedItems }); + + return (T)targetObject; + } + catch (Exception e) + { + throw new ApplicationException($"Unable to create collection resource of type {innerType.Name}: failed to add items to collection."); + } } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs index 4ca996ab..2e305621 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs @@ -24,10 +24,11 @@ namespace Stormpath.SDK.Impl.DataStore { internal interface IDataStore { - Task GetResourceAsync(string href, CancellationToken cancellationToken = default(CancellationToken)) - where T : class, IResource; + Task GetResourceAsync(string href, CancellationToken cancellationToken = default(CancellationToken)); + //where T : class, IResource; - Task> GetCollectionAsync(string href, CancellationToken cancellationToken = default(CancellationToken)); + Task> GetCollectionAsync(string href, CancellationToken cancellationToken = default(CancellationToken)); + //where T : class, IResource; Task Save(IResource resource); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceFactory.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceFactory.cs index c27ccc5c..56820b96 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceFactory.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceFactory.cs @@ -22,8 +22,8 @@ namespace Stormpath.SDK.Impl.DataStore { internal interface IResourceFactory { - T Instantiate() where T : class, IResource; + T Instantiate(); //where T : class, IResource; - T Instantiate(Hashtable properties) where T : class, IResource; + T Instantiate(Hashtable properties); //where T : class, IResource; } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs index ae4c6aba..d319c4c5 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/InternalDataStore.cs @@ -43,6 +43,9 @@ internal InternalDataStore(IRequestExecutor requestExecutor, string baseUrl) this.resourceFactory = new DefaultResourceFactory(this); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element must begin with upper-case letter", Justification = "Reviewed")] + private IInternalDataStore _this => this; + IRequestExecutor IInternalDataStore.RequestExecutor => this.requestExecutor; string IInternalDataStore.BaseUrl => this.baseUrl; @@ -64,9 +67,9 @@ async Task IDataStore.GetResourceAsync(string href, CancellationToken canc return resource; } - Task> IDataStore.GetCollectionAsync(string href, CancellationToken cancellationToken) + Task> IDataStore.GetCollectionAsync(string href, CancellationToken cancellationToken) { - throw new NotImplementedException(); + return _this.GetResourceAsync>(href, cancellationToken); } Task IDataStore.Save(IResource resource) diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetMapMarshaller.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetMapMarshaller.cs index 07b4851e..aa0307af 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetMapMarshaller.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetMapMarshaller.cs @@ -16,7 +16,12 @@ // using System.Collections; +using System.Linq; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System; +using Stormpath.SDK.Impl.Resource; namespace Stormpath.SDK.Impl.DataStore { @@ -27,14 +32,78 @@ internal sealed class JsonNetMapMarshaller : IMapSerializer public JsonNetMapMarshaller() { serializerSettings = new JsonSerializerSettings(); - serializerSettings.Converters.Add(new StormpathJsonConverter()); serializerSettings.DateParseHandling = DateParseHandling.DateTimeOffset; serializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; } Hashtable IMapSerializer.Deserialize(string json) { - return JsonConvert.DeserializeObject(json, serializerSettings); + var deserializedMap = (JObject)JsonConvert.DeserializeObject(json, serializerSettings); + var sanitizedMap = Sanitize(deserializedMap); + + return sanitizedMap; + } + + /// + /// JSON.NET deserializes everything into nested JObjects. We want Hashtables all the way down. + /// + /// Deserialized JObject from JSON.NET + /// Hashtable of primitive items, and embedded objects as Hashtables + private Hashtable Sanitize(JObject map) + { + // TODO there is probably a cleaner way of doing all of this. IDictionaries in the AbstractResource constructor? + var result = new Hashtable(map.Count); + + foreach (var prop in map.Properties()) + { + var name = prop.Name; + object value = null; + + if (prop.Value.Type == JTokenType.Array) + { + var nested = new List(); + foreach (var child in prop.Value.Children()) + { + nested.Add(Sanitize((JObject)child)); + } + + value = nested; + } + else if (prop.Value.Type == JTokenType.Object) + { + var firstChild = prop.Value.First as JProperty; + + bool isLinkProperty = prop.Value.Children().Count() == 1 + && firstChild?.Name == "href"; + if (isLinkProperty) + { + value = new LinkProperty(firstChild.Value.ToString()); + } + else + { + // Unknown object type + value = null; + } + } + else + { + if (prop.Value.Type == JTokenType.Date) + { + value = prop.Value.Value(); + } + else + { + var asString = prop.Value.ToString(); + value = string.IsNullOrEmpty(asString) + ? null + : asString; + } + } + + result.Add(name, value); + } + + return result; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/StormpathJsonConverter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/StormpathJsonConverter.cs index dccc4ccf..06e8428e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/StormpathJsonConverter.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/StormpathJsonConverter.cs @@ -19,6 +19,7 @@ using System.Collections; using Newtonsoft.Json; using Stormpath.SDK.Impl.Resource; +using System.Collections.Generic; namespace Stormpath.SDK.Impl.DataStore { @@ -43,6 +44,19 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var resultMap = new Hashtable(); while (reader.Read()) + { + ReadObjectAtCurrentDepth(reader); + } + + return resultMap; + } + + private Hashtable ReadObjectAtCurrentDepth(JsonReader reader) + { + var resultProperties = new Hashtable(); + int startingDepth = reader.Depth; + + while (reader.Read() && reader.Depth >= startingDepth) { if (reader.TokenType == JsonToken.PropertyName) { @@ -53,24 +67,53 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist switch (reader.TokenType) { case JsonToken.StartObject: - reader.Read(); - if (reader.TokenType == JsonToken.PropertyName - && reader.Value.ToString().Equals("href", StringComparison.OrdinalIgnoreCase)) + if (reader.Depth == 1) + { + reader.Read(); + if (reader.TokenType == JsonToken.PropertyName + && reader.Value.ToString().Equals("href", StringComparison.OrdinalIgnoreCase)) + { + propertyValue = new LinkProperty(reader.ReadAsString()); + } + } + else { - propertyValue = new LinkProperty(reader.ReadAsString()); + propertyValue = ReadObjectAtCurrentDepth(reader); } break; - default: + case JsonToken.Boolean: + case JsonToken.Bytes: + case JsonToken.Date: + case JsonToken.Float: + case JsonToken.Integer: + case JsonToken.String: propertyValue = reader.Value; break; + default: + continue; } - resultMap.Add(propertyName, propertyValue); + resultProperties.Add(propertyName, propertyValue); } } - return resultMap; + return resultProperties; + } + + private List ReadMultipleObjectsAtCurrentDepth(JsonReader reader) + { + var resultObjects = new List(); + var startingDepth = reader.Depth; + + while (reader.Read() + && reader.Depth == startingDepth + && reader.TokenType == JsonToken.StartObject) + { + resultObjects.Add(ReadObjectAtCurrentDepth(reader)); + } + + return resultObjects; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs index 8a07163e..1f1e65b6 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Linq/CollectionResourceQueryExecutor.cs @@ -23,27 +23,25 @@ using Stormpath.SDK.Impl.DataStore; using Stormpath.SDK.Impl.Linq.RequestModel; using Stormpath.SDK.Impl.Resource; +using Stormpath.SDK.Resource; namespace Stormpath.SDK.Impl.Linq { internal sealed class CollectionResourceQueryExecutor : IQueryExecutor { - public CollectionResourceQueryExecutor(string url, string resource, IDataStore dataStore) + public CollectionResourceQueryExecutor(string href, IDataStore dataStore) { - this.Url = url; - this.Resource = resource; + this.Href = href; this.DataStore = dataStore; } - public string Url { get; private set; } - - public string Resource { get; private set; } + public string Href { get; private set; } public IDataStore DataStore { get; private set; } public IEnumerable ExecuteCollection(CollectionResourceRequestModel requestModel) { - var asyncCollection = new CollectionResourceQueryable(this.Url, this.Resource, this.DataStore, requestModel); + var asyncCollection = new CollectionResourceQueryable(this.Href, this.DataStore, requestModel); var adapter = new Sync.SyncCollectionEnumeratorAdapter(asyncCollection); return adapter; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/AbstractResource.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/AbstractResource.cs index be68d4e4..72bae426 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/AbstractResource.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/AbstractResource.cs @@ -63,11 +63,15 @@ public T GetProperty(string name) { var value = GetProperty(name); - // Todo add a converter list to handle multiple target types + // TODO add a converter list to handle multiple target types if (typeof(T) == typeof(SDK.Account.AccountStatus)) { return (T)(SDK.Account.AccountStatus.Parse(value.ToString()) as object); } + else if (typeof(T) == typeof(SDK.Application.ApplicationStatus)) + { + return (T)(SDK.Application.ApplicationStatus.Parse(value.ToString()) as object); + } return (T)value; } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs index d665180d..7827b363 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs @@ -32,6 +32,7 @@ namespace Stormpath.SDK.Impl.Resource { internal class CollectionResourceQueryable : QueryableBase, ICollectionResourceQueryable + //where T : class, IResource { private readonly Expression expression; @@ -51,17 +52,17 @@ internal class CollectionResourceQueryable : QueryableBase, ICollectionRes private IEnumerable currentItems; - public CollectionResourceQueryable(string url, string resource, IDataStore dataStore) - : base(ExtendedQueryParser.Create(), CreateQueryExecutor(url, resource, dataStore)) + public CollectionResourceQueryable(string collectionHref, IDataStore dataStore) + : base(ExtendedQueryParser.Create(), CreateQueryExecutor(collectionHref, dataStore)) { - this.baseHref = $"{url}/{resource}"; + this.baseHref = collectionHref; this.dataStore = dataStore; } // This constructor is used for a synchronous wrapper via CollectionResourceQueryExecutor // TODO make this more SOLID and have an actual synchronous execution path that isn't a hack or wrapper - public CollectionResourceQueryable(string url, string resource, IDataStore dataStore, CollectionResourceRequestModel existingRequestModel) - : this(url, resource, dataStore) + public CollectionResourceQueryable(string collectionHref, IDataStore dataStore, CollectionResourceRequestModel existingRequestModel) + : this(collectionHref, dataStore) { this.compiledModel = existingRequestModel; } @@ -75,14 +76,14 @@ public CollectionResourceQueryable(IQueryProvider provider, Expression expressio if (relinqProvider == null || executor == null) throw new InvalidOperationException("LINQ queries must start from a supported ICollectionResourceQueryable."); - this.baseHref = $"{executor.Url}/{executor.Resource}"; + this.baseHref = executor.Href; this.dataStore = executor.DataStore; this.expression = expression; } - private static IQueryExecutor CreateQueryExecutor(string url, string resource, IDataStore dataStore) + private static IQueryExecutor CreateQueryExecutor(string href, IDataStore dataStore) { - return new CollectionResourceQueryExecutor(url, resource, dataStore); + return new CollectionResourceQueryExecutor(href, dataStore); } [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements must appear in the correct order", Justification = "Grouping internal methods above")] diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePageDto.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePage.cs similarity index 64% rename from Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePageDto.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePage.cs index 968c532b..69eb9c58 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePageDto.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResponsePage.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -16,11 +16,25 @@ // using System.Collections.Generic; +using Stormpath.SDK.Resource; namespace Stormpath.SDK.Impl.Resource { - internal sealed class CollectionResponsePageDto + internal sealed class CollectionResponsePage : IResource { + public CollectionResponsePage() + { + } + + public CollectionResponsePage(string href, int offset, int limit, int size, List items) + { + this.Href = href; + this.Offset = offset; + this.Limit = limit; + this.Size = size; + this.Items = items; + } + public string Href { get; set; } public int Offset { get; set; } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Tenant/DefaultTenant.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Tenant/DefaultTenant.cs index 74609297..08bb8eec 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Tenant/DefaultTenant.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Tenant/DefaultTenant.cs @@ -19,8 +19,10 @@ using System.Collections; using System.Threading.Tasks; using Stormpath.SDK.Application; +using Stormpath.SDK.Impl.Application; using Stormpath.SDK.Impl.DataStore; using Stormpath.SDK.Impl.Resource; +using Stormpath.SDK.Resource; using Stormpath.SDK.Tenant; namespace Stormpath.SDK.Impl.Tenant @@ -70,9 +72,9 @@ Task ITenantActions.CreateApplicationAsync(IApplication applicatio throw new NotImplementedException(); } - IApplicationAsyncList ITenantActions.GetApplications() + ICollectionResourceQueryable ITenantActions.GetApplications() { - throw new NotImplementedException(); + return new CollectionResourceQueryable(this.Applications.Href, this.GetDataStore()); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 2a3c6704..d7e9a657 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -83,6 +83,7 @@ + @@ -138,7 +139,7 @@ - + diff --git a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs index 63da0d7b..bdc64f88 100644 --- a/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs +++ b/Stormpath.SDK/Stormpath.SDK/Tenant/ITenantActions.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using Stormpath.SDK.Application; +using Stormpath.SDK.Resource; namespace Stormpath.SDK.Tenant { @@ -24,6 +25,6 @@ public interface ITenantActions { Task CreateApplicationAsync(IApplication application); - IApplicationAsyncList GetApplications(); + ICollectionResourceQueryable GetApplications(); } } From 739780621d34857a62879a118c43caa57dcd7612 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Fri, 21 Aug 2015 06:54:49 -0700 Subject: [PATCH 080/238] Clean up --- .../Impl/InternalDataStore_tests.cs | 5 +- .../Impl/Application/DefaultApplication.cs | 7 +- .../Impl/DataStore/DefaultResourceFactory.cs | 8 +- .../Impl/DataStore/IDataStore.cs | 2 - .../Impl/DataStore/IResourceFactory.cs | 4 +- .../Impl/DataStore/JsonNetMapMarshaller.cs | 4 +- .../Impl/DataStore/StormpathJsonConverter.cs | 119 ------------------ .../Resource/CollectionResourceQueryable.cs | 1 - .../Stormpath.SDK/Stormpath.SDK.csproj | 1 - 9 files changed, 11 insertions(+), 140 deletions(-) delete mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/DataStore/StormpathJsonConverter.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs index bb70c07f..e0b9eea6 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/InternalDataStore_tests.cs @@ -21,7 +21,9 @@ using NSubstitute; using Shouldly; using Stormpath.SDK.Account; +using Stormpath.SDK.Application; using Stormpath.SDK.Impl.Account; +using Stormpath.SDK.Impl.Application; using Stormpath.SDK.Impl.DataStore; using Stormpath.SDK.Impl.Http; using Stormpath.SDK.Impl.Tenant; @@ -29,8 +31,6 @@ using Stormpath.SDK.Tenant; using Stormpath.SDK.Tests.Fakes; using Xunit; -using Stormpath.SDK.Application; -using Stormpath.SDK.Impl.Application; namespace Stormpath.SDK.Tests.Impl { @@ -135,7 +135,6 @@ public async Task Instantiating_Application_from_JSON() (application as DefaultApplication).PasswordResetToken.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/passwordResetTokens"); (application as DefaultApplication).Tenant.Href.ShouldBe("https://api.stormpath.com/v1/tenants/foobarTenant"); (application as DefaultApplication).VerificationEmails.Href.ShouldBe("https://api.stormpath.com/v1/applications/foobarApplication/verificationEmails"); - } [Fact] diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.cs index 7a67092d..ecf006f7 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.cs @@ -15,15 +15,10 @@ // limitations under the License. // +using System.Collections; using Stormpath.SDK.Application; using Stormpath.SDK.Impl.DataStore; using Stormpath.SDK.Impl.Resource; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Stormpath.SDK.Impl.Application { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceFactory.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceFactory.cs index 4e4ff3e1..95f13954 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceFactory.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceFactory.cs @@ -20,12 +20,12 @@ using System.Collections.Generic; using System.Linq; using Stormpath.SDK.Account; +using Stormpath.SDK.Application; using Stormpath.SDK.Impl.Account; +using Stormpath.SDK.Impl.Application; +using Stormpath.SDK.Impl.Resource; using Stormpath.SDK.Impl.Tenant; using Stormpath.SDK.Tenant; -using Stormpath.SDK.Impl.Resource; -using Stormpath.SDK.Application; -using Stormpath.SDK.Impl.Application; namespace Stormpath.SDK.Impl.DataStore { @@ -136,7 +136,7 @@ private T InstantiateCollection(Hashtable properties) } catch (Exception e) { - throw new ApplicationException($"Unable to create collection resource of type {innerType.Name}: failed to add items to collection."); + throw new ApplicationException($"Unable to create collection resource of type {innerType.Name}: failed to add items to collection.", e); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs index 2e305621..8355f6ba 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IDataStore.cs @@ -25,10 +25,8 @@ namespace Stormpath.SDK.Impl.DataStore internal interface IDataStore { Task GetResourceAsync(string href, CancellationToken cancellationToken = default(CancellationToken)); - //where T : class, IResource; Task> GetCollectionAsync(string href, CancellationToken cancellationToken = default(CancellationToken)); - //where T : class, IResource; Task Save(IResource resource); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceFactory.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceFactory.cs index 56820b96..842a52d0 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceFactory.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceFactory.cs @@ -22,8 +22,8 @@ namespace Stormpath.SDK.Impl.DataStore { internal interface IResourceFactory { - T Instantiate(); //where T : class, IResource; + T Instantiate(); - T Instantiate(Hashtable properties); //where T : class, IResource; + T Instantiate(Hashtable properties); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetMapMarshaller.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetMapMarshaller.cs index aa0307af..c9d681a3 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetMapMarshaller.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetMapMarshaller.cs @@ -15,12 +15,12 @@ // limitations under the License. // +using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System.Collections.Generic; -using System; using Stormpath.SDK.Impl.Resource; namespace Stormpath.SDK.Impl.DataStore diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/StormpathJsonConverter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/StormpathJsonConverter.cs deleted file mode 100644 index 06e8428e..00000000 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/StormpathJsonConverter.cs +++ /dev/null @@ -1,119 +0,0 @@ -// -// Copyright (c) 2015 Stormpath, Inc. -// -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections; -using Newtonsoft.Json; -using Stormpath.SDK.Impl.Resource; -using System.Collections.Generic; - -namespace Stormpath.SDK.Impl.DataStore -{ - /// - /// Deseralizes JSON from the Stormpath API. Expects that all returned fields are: - /// primitives, or embedded Link objects with a depth of 1. - /// - internal sealed class StormpathJsonConverter : JsonConverter - { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(Hashtable); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var resultMap = new Hashtable(); - - while (reader.Read()) - { - ReadObjectAtCurrentDepth(reader); - } - - return resultMap; - } - - private Hashtable ReadObjectAtCurrentDepth(JsonReader reader) - { - var resultProperties = new Hashtable(); - int startingDepth = reader.Depth; - - while (reader.Read() && reader.Depth >= startingDepth) - { - if (reader.TokenType == JsonToken.PropertyName) - { - var propertyName = reader.Value.ToString(); - object propertyValue = null; - reader.Read(); - - switch (reader.TokenType) - { - case JsonToken.StartObject: - if (reader.Depth == 1) - { - reader.Read(); - if (reader.TokenType == JsonToken.PropertyName - && reader.Value.ToString().Equals("href", StringComparison.OrdinalIgnoreCase)) - { - propertyValue = new LinkProperty(reader.ReadAsString()); - } - } - else - { - propertyValue = ReadObjectAtCurrentDepth(reader); - } - - break; - case JsonToken.Boolean: - case JsonToken.Bytes: - case JsonToken.Date: - case JsonToken.Float: - case JsonToken.Integer: - case JsonToken.String: - propertyValue = reader.Value; - break; - default: - continue; - } - - resultProperties.Add(propertyName, propertyValue); - } - } - - return resultProperties; - } - - private List ReadMultipleObjectsAtCurrentDepth(JsonReader reader) - { - var resultObjects = new List(); - var startingDepth = reader.Depth; - - while (reader.Read() - && reader.Depth == startingDepth - && reader.TokenType == JsonToken.StartObject) - { - resultObjects.Add(ReadObjectAtCurrentDepth(reader)); - } - - return resultObjects; - } - } -} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs index 7827b363..d5353688 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Resource/CollectionResourceQueryable.cs @@ -32,7 +32,6 @@ namespace Stormpath.SDK.Impl.Resource { internal class CollectionResourceQueryable : QueryableBase, ICollectionResourceQueryable - //where T : class, IResource { private readonly Expression expression; diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index d7e9a657..514b94ff 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -101,7 +101,6 @@ - From b43fa309c2a7a54a235208af6f214e43de4c9a14 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Fri, 21 Aug 2015 07:02:25 -0700 Subject: [PATCH 081/238] Add -verbosity detailed flag to nuget restore Attempting to fix a package restore issue --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2efd1292..c3af0623 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: csharp solution: "./Stormpath.SDK/Stormpath.SDK.sln" install: - nuget restore -source "https://www.nuget.org/api/v2;https://www.myget.org/F/xunit" - ./Stormpath.SDK/Stormpath.SDK.sln + ./Stormpath.SDK/Stormpath.SDK.sln -verbosity detailed - nuget install xunit.runners -Version 1.9.2 -OutputDirectory testrunner script: - xbuild /p:Configuration=Release ./Stormpath.SDK/Stormpath.SDK.sln From 41a9f9303d9534089100e1cab4b291d990a86524 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Fri, 21 Aug 2015 09:46:42 -0700 Subject: [PATCH 082/238] Fixed issue with xUnit nuget package --- .travis.yml | 3 +-- Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj | 4 ++-- Stormpath.SDK/Stormpath.SDK.Tests/packages.config | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2efd1292..6797e1c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: csharp solution: "./Stormpath.SDK/Stormpath.SDK.sln" install: -- nuget restore -source "https://www.nuget.org/api/v2;https://www.myget.org/F/xunit" - ./Stormpath.SDK/Stormpath.SDK.sln +- nuget restore ./Stormpath.SDK/Stormpath.SDK.sln - nuget install xunit.runners -Version 1.9.2 -OutputDirectory testrunner script: - xbuild /p:Configuration=Release ./Stormpath.SDK/Stormpath.SDK.sln diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 98b925d3..74dbc648 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -1,6 +1,6 @@  - + Debug @@ -132,8 +132,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/packages.config b/Stormpath.SDK/Stormpath.SDK.Tests/packages.config index a8c31342..3ad7cf45 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/packages.config +++ b/Stormpath.SDK/Stormpath.SDK.Tests/packages.config @@ -8,5 +8,5 @@ - + \ No newline at end of file From e9d0fdbad25940c652bfab34fa389e86828bf609 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Fri, 21 Aug 2015 09:56:19 -0700 Subject: [PATCH 083/238] Rollback previous edit that had the dumbs --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c3af0623..00561de0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: csharp solution: "./Stormpath.SDK/Stormpath.SDK.sln" install: -- nuget restore -source "https://www.nuget.org/api/v2;https://www.myget.org/F/xunit" - ./Stormpath.SDK/Stormpath.SDK.sln -verbosity detailed +- nuget restore ./Stormpath.SDK/Stormpath.SDK.sln -verbosity detailed - nuget install xunit.runners -Version 1.9.2 -OutputDirectory testrunner script: - xbuild /p:Configuration=Release ./Stormpath.SDK/Stormpath.SDK.sln From d793e34e0a22fdfc73da7b3bf1b0f94f72d2cb8b Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Fri, 21 Aug 2015 10:10:14 -0700 Subject: [PATCH 084/238] Attempt to use xUnit 2.0 runner beta --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 00561de0..b7384e90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,11 @@ language: csharp solution: "./Stormpath.SDK/Stormpath.SDK.sln" install: - nuget restore ./Stormpath.SDK/Stormpath.SDK.sln -verbosity detailed -- nuget install xunit.runners -Version 1.9.2 -OutputDirectory testrunner +- nuget install xunit.runner.console -pre -OutputDirectory testrunner script: - xbuild /p:Configuration=Release ./Stormpath.SDK/Stormpath.SDK.sln -- mono ./testrunner/xunit.runners.1.9.2/tools/xunit.console.clr4.exe ./Stormpath.SDK/Stormpath.SDK.Tests/bin/Release/Stormpath.SDK.Tests.dll -- mono ./testrunner/xunit.runners.1.9.2/tools/xunit.console.clr4.exe ./Stormpath.SDK/Stormpath.SDK.Tests.Integration/bin/Release/Stormpath.SDK.Tests.Integration.dll +- mono ./testrunner/xunit.runner.console*/tools/xunit.console.exe ./Stormpath.SDK/Stormpath.SDK.Tests/bin/Release/Stormpath.SDK.Tests.dll +- mono ./testrunner/xunit.runner.console*/tools/xunit.console.exe ./Stormpath.SDK/Stormpath.SDK.Tests.Integration/bin/Release/Stormpath.SDK.Tests.Integration.dll env: global: - secure: sMEfLcgXwYvRsaVRFVDzHQoMCNwgtC39F4VQe3fdq+q4oIcOYn1+e/80ZgfP3JqSTcJyhEtNfQLU1D3rYa5mfCsZjeU9EHabK1fheMRgMY9VMR24v1SG/54wXY9teBnUm1BIi+/yFdybl8a02KsvhwWqHhFVthJQqVV4y7SnQk8BKJJ/75b2Vr/Rq6W6DSXPeZoXif+uvNbFkngE7Dy8d3nhjOybh9aXSv3oWyMKIrR5zvMCNCHsPeW2+X+3x5Cr8ryMnuQ+b3tX8qlPhxyeUvcTlFcCuZYHHs/4CTHL9zEcaAkU4bYg1zfDRDHiJ/e3h591KfVSS+E/1mh29XsnE1NMIbWHKJpNB0KlW5z4qO7t7Atgsb9FOuvvqCYFvkI9WmM08ilhTt369sEXNKsEkLTsAtYH2igWng7J6meAnj5fyRcN7d8VlPRPyUbzW8RFKQn69wF7nmfU2wYQaRfbiDpYOcHOxtOA2SNuYVG2Ysf6JMIgWzeKcRwC6HWjPZu/kNk6+wGQv2d2VSapex1/tXazB8+8cpjCGK6Kqw115Y01JZ6ePfj584Wfstr3XlVSfeu9hCvGhO5EhLBq6KjL5TdfnkaFtq2CFsSHjYHumQNwW0rgmcL6nqs2SrG/feut5GUSVqZKgy9xy/7TOxoHwrnTXZqu4TgOSzpN82oNXkg= From cf98ef31966530399f9e029ac8097351c848474c Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Fri, 21 Aug 2015 10:38:32 -0700 Subject: [PATCH 085/238] Let's try xUnit 2.0! --- .travis.yml | 5 ++-- .../Stormpath.SDK.Tests.Integration.csproj | 30 ++++++++++++++----- .../packages.config | 8 +++-- .../Stormpath.SDK.Tests.csproj | 19 +++++++++--- .../Stormpath.SDK.Tests/Theory_tests.cs | 24 +++++++++++++++ .../Stormpath.SDK.Tests/packages.config | 9 ++++-- 6 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Theory_tests.cs diff --git a/.travis.yml b/.travis.yml index b7384e90..e06bc691 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,10 @@ language: csharp solution: "./Stormpath.SDK/Stormpath.SDK.sln" install: - nuget restore ./Stormpath.SDK/Stormpath.SDK.sln -verbosity detailed -- nuget install xunit.runner.console -pre -OutputDirectory testrunner script: - xbuild /p:Configuration=Release ./Stormpath.SDK/Stormpath.SDK.sln -- mono ./testrunner/xunit.runner.console*/tools/xunit.console.exe ./Stormpath.SDK/Stormpath.SDK.Tests/bin/Release/Stormpath.SDK.Tests.dll -- mono ./testrunner/xunit.runner.console*/tools/xunit.console.exe ./Stormpath.SDK/Stormpath.SDK.Tests.Integration/bin/Release/Stormpath.SDK.Tests.Integration.dll +- mono ./packages/xunit.runner.console*/tools/xunit.console.exe ./Stormpath.SDK/Stormpath.SDK.Tests/bin/Release/Stormpath.SDK.Tests.dll +- mono ./packages/xunit.runner.console*/tools/xunit.console.exe ./Stormpath.SDK/Stormpath.SDK.Tests.Integration/bin/Release/Stormpath.SDK.Tests.Integration.dll env: global: - secure: sMEfLcgXwYvRsaVRFVDzHQoMCNwgtC39F4VQe3fdq+q4oIcOYn1+e/80ZgfP3JqSTcJyhEtNfQLU1D3rYa5mfCsZjeU9EHabK1fheMRgMY9VMR24v1SG/54wXY9teBnUm1BIi+/yFdybl8a02KsvhwWqHhFVthJQqVV4y7SnQk8BKJJ/75b2Vr/Rq6W6DSXPeZoXif+uvNbFkngE7Dy8d3nhjOybh9aXSv3oWyMKIrR5zvMCNCHsPeW2+X+3x5Cr8ryMnuQ+b3tX8qlPhxyeUvcTlFcCuZYHHs/4CTHL9zEcaAkU4bYg1zfDRDHiJ/e3h591KfVSS+E/1mh29XsnE1NMIbWHKJpNB0KlW5z4qO7t7Atgsb9FOuvvqCYFvkI9WmM08ilhTt369sEXNKsEkLTsAtYH2igWng7J6meAnj5fyRcN7d8VlPRPyUbzW8RFKQn69wF7nmfU2wYQaRfbiDpYOcHOxtOA2SNuYVG2Ysf6JMIgWzeKcRwC6HWjPZu/kNk6+wGQv2d2VSapex1/tXazB8+8cpjCGK6Kqw115Y01JZ6ePfj584Wfstr3XlVSfeu9hCvGhO5EhLBq6KjL5TdfnkaFtq2CFsSHjYHumQNwW0rgmcL6nqs2SrG/feut5GUSVqZKgy9xy/7TOxoHwrnTXZqu4TgOSzpN82oNXkg= diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj index a0910b0c..4a45925f 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj @@ -1,5 +1,6 @@  + Debug @@ -12,6 +13,8 @@ v4.5 512 + + true @@ -54,8 +57,16 @@ - - ..\packages\xunit.1.9.2\lib\net20\xunit.dll + + ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True + + + ..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll True @@ -76,7 +87,9 @@ - + + Designer + @@ -85,11 +98,14 @@ - - - - + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/packages.config b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/packages.config new file mode 100644 index 00000000..fb362709 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/stylecop.json b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/stylecop.json new file mode 100644 index 00000000..1ce61c72 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/stylecop.json @@ -0,0 +1,8 @@ +{ + "settings": { + "documentationRules": { + "companyName": "Stormpath, Inc.", + "copyrightText": "Copyright (c) 2015 Stormpath, Inc." + } + } +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetSerializer.cs b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/DefaultJsonNetSerializer.cs similarity index 94% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetSerializer.cs rename to Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/DefaultJsonNetSerializer.cs index 9ddc823f..cbf8b61c 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonNetSerializer.cs +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/DefaultJsonNetSerializer.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -21,13 +21,13 @@ using Newtonsoft.Json.Linq; using Stormpath.SDK.Serialization; -namespace Stormpath.SDK.Impl.DataStore +namespace Stormpath.SDK.JsonNetSerializer { - internal sealed class JsonNetSerializer : IJsonSerializer + public sealed class DefaultJsonNetSerializer : IJsonSerializer { private readonly JsonSerializerSettings serializerSettings; - public JsonNetSerializer() + public DefaultJsonNetSerializer() { this.serializerSettings = new JsonSerializerSettings(); this.serializerSettings.DateParseHandling = DateParseHandling.DateTimeOffset; diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..d6c4e918 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Properties/AssemblyInfo.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Stormpath.SDK.JsonNetSerializer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stormpath.SDK.JsonNetSerializer")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4011b5ea-ceb5-4212-b8e6-d18c63ab6acf")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Stormpath.SDK.JsonNetSerializer.csproj b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Stormpath.SDK.JsonNetSerializer.csproj new file mode 100644 index 00000000..3cc69b81 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Stormpath.SDK.JsonNetSerializer.csproj @@ -0,0 +1,84 @@ + + + + + Debug + AnyCPU + {4011B5EA-CEB5-4212-B8E6-D18C63AB6ACF} + Library + Properties + Stormpath.SDK.JsonNetSerializer + Stormpath.SDK.JsonNetSerializer + v4.5 + 512 + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + ..\Stormpath.SDK\Stormpath.SDK.ruleset + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + {79a65c37-9db1-413a-ac23-708404530295} + Stormpath.SDK + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/packages.config b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/packages.config new file mode 100644 index 00000000..21745566 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/stylecop.json b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/stylecop.json new file mode 100644 index 00000000..1ce61c72 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/stylecop.json @@ -0,0 +1,8 @@ +{ + "settings": { + "documentationRules": { + "companyName": "Stormpath, Inc.", + "copyrightText": "Copyright (c) 2015 Stormpath, Inc." + } + } +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/IntegrationTestFixture.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/IntegrationTestFixture.cs index 9fa17d56..1ec994f5 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/IntegrationTestFixture.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/IntegrationTestFixture.cs @@ -33,6 +33,8 @@ public class IntegrationTestFixture : IDisposable { private IntegrationTestData testData; + private bool isDisposed = false; + public IntegrationTestFixture() { this.testData = new IntegrationTestData(); @@ -45,10 +47,25 @@ public IntegrationTestFixture() .GetAwaiter().GetResult(); } + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.RemoveObjectsFromTenantAsync() + .GetAwaiter().GetResult(); + } + + this.isDisposed = true; + } + } + + // This code added to correctly implement the disposable pattern. public void Dispose() { - this.RemoveObjectsFromTenantAsync() - .GetAwaiter().GetResult(); + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(true); } public string TenantHref { get; private set; } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj index 84ac040a..37f94413 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj @@ -25,6 +25,7 @@ prompt 4 ..\Stormpath.SDK\Stormpath.SDK.ruleset + true pdbonly @@ -91,6 +92,10 @@ + + {4011b5ea-ceb5-4212-b8e6-d18c63ab6acf} + Stormpath.SDK.JsonNetSerializer + {79a65c37-9db1-413a-ac23-708404530295} Stormpath.SDK diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/App.config b/Stormpath.SDK/Stormpath.SDK.Tests/App.config index 295421b9..d0998f24 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/App.config +++ b/Stormpath.SDK/Stormpath.SDK.Tests/App.config @@ -1,6 +1,6 @@ - + - + - \ No newline at end of file + diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeDataStore.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeDataStore.cs index a875aa69..1200e33a 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/FakeDataStore.cs @@ -30,7 +30,7 @@ namespace Stormpath.SDK.Tests.Fakes { // TODO: Make this an actual server with valid responses - public class FakeDataStore : IInternalDataStore + public sealed class FakeDataStore : IInternalDataStore { private static int defaultLimit = 25; private static int defaultOffset = 0; diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/StubDataStore.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/StubDataStore.cs index 1f9b2a44..f2006a4e 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/StubDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/StubDataStore.cs @@ -15,18 +15,20 @@ // limitations under the License. // +using System; using System.Threading; using System.Threading.Tasks; using Stormpath.SDK.DataStore; using Stormpath.SDK.Impl.DataStore; using Stormpath.SDK.Impl.Http; using Stormpath.SDK.Impl.Resource; +using Stormpath.SDK.JsonNetSerializer; using Stormpath.SDK.Resource; using Stormpath.SDK.Shared; namespace Stormpath.SDK.Tests.Fakes { - public class StubDataStore : IInternalDataStore + public sealed class StubDataStore : IInternalDataStore, IDisposable { private readonly IInternalDataStore fakeDataStore; @@ -37,7 +39,7 @@ public StubDataStore(string resourceJson, string baseHref, ILogger logger = null ? new SDK.Impl.NullLogger() : logger; - this.fakeDataStore = new DefaultDataStore(fakeRequestExecutor.Object, baseHref, new JsonNetSerializer(), useLogger); + this.fakeDataStore = new DefaultDataStore(fakeRequestExecutor.Object, baseHref, new DefaultJsonNetSerializer(), useLogger); } string IInternalDataStore.BaseUrl => this.fakeDataStore.BaseUrl; @@ -77,5 +79,33 @@ public StubDataStore(string resourceJson, string baseHref, ILogger logger = null T IInternalDataStore.Save(T resource) => this.fakeDataStore.Save(resource); Task IInternalDataStore.SaveAsync(T resource, CancellationToken cancellationToken) => this.fakeDataStore.SaveAsync(resource, cancellationToken); + +#pragma warning disable SA1124 // Do not use regions + #region IDisposable Support +#pragma warning restore SA1124 // Do not use regions + + private bool isDisposed = false; // To detect redundant calls + + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + (this.fakeDataStore as DefaultDataStore).Dispose(); + } + + this.isDisposed = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(true); + } + + #endregion } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Cache/InMemoryCache_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Cache/InMemoryCache_tests.cs index 8d232477..168f9b38 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Cache/InMemoryCache_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Cache/InMemoryCache_tests.cs @@ -29,18 +29,34 @@ namespace Stormpath.SDK.Tests.Impl.Cache public class InMemoryCache_tests : IDisposable { private readonly IDisposable dummyCache; + private bool isDisposed = false; public InMemoryCache_tests() { this.dummyCache = new InMemoryCache("dummy"); } + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + // This is done for testing because the Dispose() method tears down the + // backing memory cache. This will be executed after each test run + // to ensure that each test is idempotent. + this.dummyCache.Dispose(); + } + + this.isDisposed = true; + } + } + + // This code added to correctly implement the disposable pattern. public void Dispose() { - // This is done for testing because the Dispose() method tears down the - // backing memory cache. This will be executed after each test run - // to ensure that each test is idempotent. - this.dummyCache.Dispose(); + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(true); } public class SynchronousCache diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/CreationOptions_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/CreationOptions_tests.cs index 9b837f7a..399b6e61 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/CreationOptions_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/CreationOptions_tests.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System; using System.Threading; using NSubstitute; using Stormpath.SDK.Account; @@ -43,7 +44,7 @@ private static void VerifyRequestContents(IRequestExecutor reqex, string querySt Arg.Any()).IgnoreAwait(); } - public class Application_options + public class Application_options : IDisposable { private readonly IInternalDataStore dataStore; @@ -93,9 +94,31 @@ public void Create_application_request_with_named_directory() this.VerifyThat(options, resultsInQueryString: "?createDirectory=Foobar+Directory"); } + + private bool isDisposed = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + (this.dataStore as StubDataStore).Dispose(); + } + + this.isDisposed = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(true); + } } - public class Account_options + public class Account_options : IDisposable { private readonly IInternalDataStore dataStore; @@ -143,6 +166,28 @@ public void Create_with_workflow_override_disabled() this.VerifyThat(options, resultsInQueryString: "?registrationWorkflowEnabled=false"); } + + private bool isDisposed = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + (this.dataStore as StubDataStore).Dispose(); + } + + this.isDisposed = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(true); + } } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs index 80640328..4ea8f54b 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs @@ -21,6 +21,8 @@ using Stormpath.SDK.Api; using Stormpath.SDK.Client; using Stormpath.SDK.Impl.Client; +using Stormpath.SDK.Impl.Serialization; +using Stormpath.SDK.Serialization; using Xunit; namespace Stormpath.SDK.Tests.Impl @@ -136,5 +138,29 @@ public void Throws_when_ConnectionTimeout_is_negative() .Build(); }); } + + [Fact] + public void Throws_when_no_JSON_serializer_can_be_found() + { + IJsonSerializer dummy = null; + var fakeLoader = Substitute.For(); + fakeLoader + .TryLoad(out dummy) + .Returns(false); + + var fakeKey = Substitute.For(); + fakeKey.IsValid().Returns(true); + + IClientBuilder builder = new DefaultClientBuilder(fakeLoader); + + Assert.Throws(() => + { + var client = builder + .SetApiKey(fakeKey) + .SetAuthenticationScheme(AuthenticationScheme.SAuthc1) + .SetConnectionTimeout(10) + .Build(); + }); + } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultJsonSerializerLoader_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultJsonSerializerLoader_tests.cs new file mode 100644 index 00000000..78a42fcf --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultJsonSerializerLoader_tests.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Shouldly; +using Stormpath.SDK.Impl.Serialization; +using Stormpath.SDK.Serialization; +using Xunit; + +namespace Stormpath.SDK.Tests.Impl +{ + public class DefaultJsonSerializerLoader_tests + { + [Fact] + public void Default_library_is_loaded() + { + IJsonSerializerLoader loader = new DefaultJsonSerializerLoader(); + + // This test project has a reference to Stormpath.SDK.JsonNetSerializer, so the file lookup will succeed + IJsonSerializer serializer = null; + bool loadResult = loader.TryLoad(out serializer); + + loadResult.ShouldBe(true); + serializer.ShouldNotBe(null); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FieldConverterList_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FieldConverterList_tests.cs index b9122f32..85caadb0 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FieldConverterList_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FieldConverterList_tests.cs @@ -19,7 +19,7 @@ using System.Collections.Generic; using Shouldly; using Stormpath.SDK.Account; -using Stormpath.SDK.Impl.DataStore.FieldConverters; +using Stormpath.SDK.Impl.Serialization.FieldConverters; using Xunit; namespace Stormpath.SDK.Tests.Impl diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FieldConverter_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FieldConverter_tests.cs index a05c9c84..cbf388ab 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FieldConverter_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FieldConverter_tests.cs @@ -22,8 +22,8 @@ using Stormpath.SDK.Application; using Stormpath.SDK.Directory; using Stormpath.SDK.Group; -using Stormpath.SDK.Impl.DataStore.FieldConverters; using Stormpath.SDK.Impl.Resource; +using Stormpath.SDK.Impl.Serialization.FieldConverters; using Xunit; namespace Stormpath.SDK.Tests.Impl diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 6c4792b6..02a8315f 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -26,6 +26,7 @@ prompt 4 ..\Stormpath.SDK\Stormpath.SDK.ruleset + true pdbonly @@ -64,6 +65,7 @@ True + @@ -90,6 +92,7 @@ + @@ -152,6 +155,10 @@ + + {4011b5ea-ceb5-4212-b8e6-d18c63ab6acf} + Stormpath.SDK.JsonNetSerializer + {79a65c37-9db1-413a-ac23-708404530295} Stormpath.SDK diff --git a/Stormpath.SDK/Stormpath.SDK.sln b/Stormpath.SDK/Stormpath.SDK.sln index 4d4763ad..abb2dc61 100644 --- a/Stormpath.SDK/Stormpath.SDK.sln +++ b/Stormpath.SDK/Stormpath.SDK.sln @@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.Tests", "Stor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.Tests.Integration", "Stormpath.SDK.Tests.Integration\Stormpath.SDK.Tests.Integration.csproj", "{09B79235-D67F-4FCA-9991-5AE30838EECE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.JsonNetSerializer", "Stormpath.SDK.JsonNetSerializer\Stormpath.SDK.JsonNetSerializer.csproj", "{4011B5EA-CEB5-4212-B8E6-D18C63AB6ACF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.JsonNetSerializer.Tests", "Stormpath.SDK.JsonNetSerializer.Tests\Stormpath.SDK.JsonNetSerializer.Tests.csproj", "{0B3F40E7-83A9-481B-A694-78D02D735ACF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +37,14 @@ Global {09B79235-D67F-4FCA-9991-5AE30838EECE}.Debug|Any CPU.Build.0 = Debug|Any CPU {09B79235-D67F-4FCA-9991-5AE30838EECE}.Release|Any CPU.ActiveCfg = Release|Any CPU {09B79235-D67F-4FCA-9991-5AE30838EECE}.Release|Any CPU.Build.0 = Release|Any CPU + {4011B5EA-CEB5-4212-B8E6-D18C63AB6ACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4011B5EA-CEB5-4212-B8E6-D18C63AB6ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4011B5EA-CEB5-4212-B8E6-D18C63AB6ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4011B5EA-CEB5-4212-B8E6-D18C63AB6ACF}.Release|Any CPU.Build.0 = Release|Any CPU + {0B3F40E7-83A9-481B-A694-78D02D735ACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B3F40E7-83A9-481B-A694-78D02D735ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B3F40E7-83A9-481B-A694-78D02D735ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B3F40E7-83A9-481B-A694-78D02D735ACF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs index 6852fd95..3bd5bd81 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs @@ -18,6 +18,7 @@ using System; using Stormpath.SDK.Api; using Stormpath.SDK.Client; +using Stormpath.SDK.Impl.Serialization; using Stormpath.SDK.Serialization; using Stormpath.SDK.Shared; @@ -25,6 +26,8 @@ namespace Stormpath.SDK.Impl.Client { internal sealed class DefaultClientBuilder : IClientBuilder { + private readonly IJsonSerializerLoader serializerLoader; + private string baseUrl = DefaultClient.DefaultBaseUrl; private int connectionTimeout = DefaultClient.DefaultConnectionTimeout; private AuthenticationScheme authenticationScheme; @@ -32,6 +35,16 @@ internal sealed class DefaultClientBuilder : IClientBuilder private IJsonSerializer jsonSerializer; private ILogger logger; + public DefaultClientBuilder() + : this(new DefaultJsonSerializerLoader()) + { + } + + internal DefaultClientBuilder(IJsonSerializerLoader serializerLoader) + { + this.serializerLoader = serializerLoader; + } + IClientBuilder IClientBuilder.SetApiKey(IClientApiKey apiKey) { if (apiKey == null) @@ -85,10 +98,10 @@ IClient IClientBuilder.Build() var useSerializer = this.jsonSerializer; bool isValidSerializer = this.jsonSerializer != null || - DefaultJsonSerializer.TryLoad(out useSerializer); + this.serializerLoader.TryLoad(out useSerializer); if (!isValidSerializer) - throw new ApplicationException("Could not find a valid JSON serializer."); + throw new ApplicationException("Could not find a valid JSON serializer. Include Stormpath.SDK.JsonNetSerializer.dll in the application path."); return new DefaultClient(this.apiKey, this.baseUrl, this.authenticationScheme, this.connectionTimeout, useSerializer, this.logger); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs index 411cd9c7..ad87002e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs @@ -29,6 +29,7 @@ using Stormpath.SDK.Impl.Http; using Stormpath.SDK.Impl.Http.Support; using Stormpath.SDK.Impl.Resource; +using Stormpath.SDK.Impl.Serialization; using Stormpath.SDK.Resource; using Stormpath.SDK.Serialization; using Stormpath.SDK.Shared; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/DefaultJsonSerializerLoader.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/DefaultJsonSerializerLoader.cs new file mode 100644 index 00000000..9ee1f09a --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/DefaultJsonSerializerLoader.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.IO; +using System.Reflection; +using Stormpath.SDK.Serialization; + +namespace Stormpath.SDK.Impl.Serialization +{ + internal sealed class DefaultJsonSerializerLoader : IJsonSerializerLoader + { + bool IJsonSerializerLoader.TryLoad(out IJsonSerializer serializer) + { + var absolutePath = Path.GetFullPath("Stormpath.SDK.JsonNetSerializer.dll"); + + if (!File.Exists(absolutePath)) + { + serializer = null; + return false; + } + + Assembly assembly = Assembly.LoadFile(absolutePath); + serializer = assembly.CreateInstance("Stormpath.SDK.JsonNetSerializer.DefaultJsonNetSerializer") as IJsonSerializer; + + return serializer != null; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/AbstractFieldConverter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/AbstractFieldConverter.cs similarity index 95% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/AbstractFieldConverter.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/AbstractFieldConverter.cs index 5ed74071..2d3f0830 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/AbstractFieldConverter.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/AbstractFieldConverter.cs @@ -17,8 +17,9 @@ using System; using System.Collections.Generic; +using Stormpath.SDK.Impl.DataStore; -namespace Stormpath.SDK.Impl.DataStore.FieldConverters +namespace Stormpath.SDK.Impl.Serialization.FieldConverters { internal abstract class AbstractFieldConverter { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/FieldConverterList.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/FieldConverterList.cs similarity index 97% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/FieldConverterList.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/FieldConverterList.cs index e4a2b811..ce160974 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/FieldConverterList.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/FieldConverterList.cs @@ -19,7 +19,7 @@ using System.Collections.Generic; using System.Linq; -namespace Stormpath.SDK.Impl.DataStore.FieldConverters +namespace Stormpath.SDK.Impl.Serialization.FieldConverters { internal sealed class FieldConverterList { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/FieldConverterResult.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/FieldConverterResult.cs similarity index 96% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/FieldConverterResult.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/FieldConverterResult.cs index d462eaec..755222e3 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/FieldConverterResult.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/FieldConverterResult.cs @@ -18,7 +18,7 @@ using System; using Stormpath.SDK.Impl.Utility; -namespace Stormpath.SDK.Impl.DataStore.FieldConverters +namespace Stormpath.SDK.Impl.Serialization.FieldConverters { internal sealed class FieldConverterResult : ImmutableValueObject { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/FuncFieldConverter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/FuncFieldConverter.cs similarity index 95% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/FuncFieldConverter.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/FuncFieldConverter.cs index a3b06296..8938eb86 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/FuncFieldConverter.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/FuncFieldConverter.cs @@ -18,7 +18,7 @@ using System; using System.Collections.Generic; -namespace Stormpath.SDK.Impl.DataStore.FieldConverters +namespace Stormpath.SDK.Impl.Serialization.FieldConverters { internal sealed class FuncFieldConverter : AbstractFieldConverter { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/LinkPropertyConverter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/LinkPropertyConverter.cs similarity index 96% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/LinkPropertyConverter.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/LinkPropertyConverter.cs index 05fa78cc..86fd940e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/LinkPropertyConverter.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/LinkPropertyConverter.cs @@ -20,7 +20,7 @@ using System.Linq; using Stormpath.SDK.Impl.Resource; -namespace Stormpath.SDK.Impl.DataStore.FieldConverters +namespace Stormpath.SDK.Impl.Serialization.FieldConverters { internal sealed class LinkPropertyConverter : AbstractFieldConverter { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/StatusFieldConverters.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/StatusFieldConverters.cs similarity index 98% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/StatusFieldConverters.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/StatusFieldConverters.cs index 4f4ea820..c2c71f4a 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FieldConverters/StatusFieldConverters.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/FieldConverters/StatusFieldConverters.cs @@ -18,7 +18,7 @@ using System; using System.Collections.Generic; -namespace Stormpath.SDK.Impl.DataStore.FieldConverters +namespace Stormpath.SDK.Impl.Serialization.FieldConverters { internal static class StatusFieldConverters { diff --git a/Stormpath.SDK/Stormpath.SDK/Serialization/DefaultJsonSerializer.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/IJsonSerializerLoader.cs similarity index 62% rename from Stormpath.SDK/Stormpath.SDK/Serialization/DefaultJsonSerializer.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Serialization/IJsonSerializerLoader.cs index d56d0e07..ac766c17 100644 --- a/Stormpath.SDK/Stormpath.SDK/Serialization/DefaultJsonSerializer.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/IJsonSerializerLoader.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -15,18 +15,12 @@ // limitations under the License. // -using Stormpath.SDK.Impl.DataStore; +using Stormpath.SDK.Serialization; -namespace Stormpath.SDK.Serialization +namespace Stormpath.SDK.Impl.Serialization { - public static class DefaultJsonSerializer + internal interface IJsonSerializerLoader { - public static bool TryLoad(out IJsonSerializer serializer) - { - // TODO load by runtime assembly binding - serializer = new JsonNetSerializer(); - - return true; - } + bool TryLoad(out IJsonSerializer serializer); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonSerializationProvider.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/JsonSerializationProvider.cs similarity index 96% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonSerializationProvider.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Serialization/JsonSerializationProvider.cs index d6723ac0..a91b9ce8 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/JsonSerializationProvider.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Serialization/JsonSerializationProvider.cs @@ -17,10 +17,11 @@ using System; using System.Collections.Generic; -using Stormpath.SDK.Impl.DataStore.FieldConverters; +using Stormpath.SDK.Impl.DataStore; +using Stormpath.SDK.Impl.Serialization.FieldConverters; using Stormpath.SDK.Serialization; -namespace Stormpath.SDK.Impl.DataStore +namespace Stormpath.SDK.Impl.Serialization { internal sealed class JsonSerializationProvider { diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index 2bf3ae8e..ebc84082 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -38,10 +38,6 @@ stormpath.sdk.ruleset - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - True - ..\packages\Remotion.Linq.1.15.15.0\lib\portable-net45+wp80+wpa81+win\Remotion.Linq.dll True @@ -127,13 +123,13 @@ - - - + + + - - - + + + @@ -143,8 +139,9 @@ - - + + + @@ -183,7 +180,6 @@ - diff --git a/Stormpath.SDK/Stormpath.SDK/packages.config b/Stormpath.SDK/Stormpath.SDK/packages.config index 1033ffa7..625d7c47 100644 --- a/Stormpath.SDK/Stormpath.SDK/packages.config +++ b/Stormpath.SDK/Stormpath.SDK/packages.config @@ -4,7 +4,6 @@ - \ No newline at end of file From e6ab33e36c1259750837164f37043c0818ff6943 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 10 Sep 2015 12:58:37 -0700 Subject: [PATCH 165/238] Add tests from JsonNetSerializer.Tests project --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index df6670cc..c30e3372 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ script: - xbuild /p:Configuration=Release ./Stormpath.SDK/Stormpath.SDK.sln - mono ./Stormpath.SDK/packages/xunit.runner.console*/tools/xunit.console.exe ./Stormpath.SDK/Stormpath.SDK.Tests/bin/Release/Stormpath.SDK.Tests.dll -noappdomain - mono ./Stormpath.SDK/packages/xunit.runner.console*/tools/xunit.console.exe ./Stormpath.SDK/Stormpath.SDK.Tests.Integration/bin/Release/Stormpath.SDK.Tests.Integration.dll -noappdomain +- mono ./Stormpath.SDK/packages/xunit.runner.console*/tools/xunit.console.exe ./Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/bin/Release/Stormpath.SDK.JsonNetSerializer.Tests.dll -noappdomain env: global: - secure: sMEfLcgXwYvRsaVRFVDzHQoMCNwgtC39F4VQe3fdq+q4oIcOYn1+e/80ZgfP3JqSTcJyhEtNfQLU1D3rYa5mfCsZjeU9EHabK1fheMRgMY9VMR24v1SG/54wXY9teBnUm1BIi+/yFdybl8a02KsvhwWqHhFVthJQqVV4y7SnQk8BKJJ/75b2Vr/Rq6W6DSXPeZoXif+uvNbFkngE7Dy8d3nhjOybh9aXSv3oWyMKIrR5zvMCNCHsPeW2+X+3x5Cr8ryMnuQ+b3tX8qlPhxyeUvcTlFcCuZYHHs/4CTHL9zEcaAkU4bYg1zfDRDHiJ/e3h591KfVSS+E/1mh29XsnE1NMIbWHKJpNB0KlW5z4qO7t7Atgsb9FOuvvqCYFvkI9WmM08ilhTt369sEXNKsEkLTsAtYH2igWng7J6meAnj5fyRcN7d8VlPRPyUbzW8RFKQn69wF7nmfU2wYQaRfbiDpYOcHOxtOA2SNuYVG2Ysf6JMIgWzeKcRwC6HWjPZu/kNk6+wGQv2d2VSapex1/tXazB8+8cpjCGK6Kqw115Y01JZ6ePfj584Wfstr3XlVSfeu9hCvGhO5EhLBq6KjL5TdfnkaFtq2CFsSHjYHumQNwW0rgmcL6nqs2SrG/feut5GUSVqZKgy9xy/7TOxoHwrnTXZqu4TgOSzpN82oNXkg= From 2960b7b7a722290d7a31aa8c3f2f9dbf58994108 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 10 Sep 2015 14:31:03 -0700 Subject: [PATCH 166/238] Finished caching interface with DataStore (progress on #4) --- .../Fakes/StubDataStore.cs | 3 +- .../Impl/Cache/DefaultCacheResolver_tests.cs | 21 +++-- .../DefaultCacheProviderResolver_tests.cs | 75 +++++++++++++++ .../Impl/DefaultClientBuilder_tests.cs | 18 +++- .../Impl/FilterChain_tests.cs | 33 +++---- .../Stormpath.SDK.Tests.csproj | 1 + ...nager.cs => IAsynchronousCacheProvider.cs} | 4 +- .../{ICacheManager.cs => ICacheProvider.cs} | 4 +- ...anager.cs => ISynchronousCacheProvider.cs} | 4 +- .../Stormpath.SDK/Client/IClientBuilder.cs | 1 - .../Impl/Cache/DefaultCacheResolver.cs | 40 ++++---- .../Impl/Cache/ICacheResolver.cs | 10 +- .../Stormpath.SDK/Impl/Cache/InMemoryCache.cs | 18 ++-- .../Impl/Cache/InMemoryCacheManager.cs | 83 +++++++++++++++-- .../Impl/Cache/InMemoryCacheProvider.cs | 83 ++--------------- ...llCacheManager.cs => NullCacheProvider.cs} | 12 +-- .../Client/DefaultCacheProviderResolver.cs | 40 ++++++++ .../Impl/Client/DefaultClient.cs | 20 ++-- .../Impl/Client/DefaultClientBuilder.cs | 25 ++++- .../Impl/DataStore/DefaultDataStore.cs | 65 +++++++------ .../DataStore/DefaultResourceDataRequest.cs | 11 ++- .../DefaultAsynchronousFilter.cs | 4 +- .../DefaultAsynchronousFilterChain.cs | 6 +- .../DefaultSynchronousFilter.cs | 4 +- .../DefaultSynchronousFilterChain.cs | 8 +- .../IAsynchronousFilter.cs | 4 +- .../IAsynchronousFilterChain.cs | 2 +- .../ISynchronousFilter.cs | 4 +- .../ISynchronousFilterChain.cs | 4 +- .../Impl/DataStore/Filters/ReadCacheFilter.cs | 92 +++++++++++++++++++ .../DataStore/Filters/WriteCacheFilter.cs | 46 ++++++++++ .../Impl/DataStore/IResourceDataRequest.cs | 3 + .../Stormpath.SDK/Impl/InternalFactory.cs | 5 +- .../Stormpath.SDK/Stormpath.SDK.csproj | 33 +++---- 34 files changed, 543 insertions(+), 243 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultCacheProviderResolver_tests.cs rename Stormpath.SDK/Stormpath.SDK/Cache/{IAsynchronousCacheManager.cs => IAsynchronousCacheProvider.cs} (90%) rename Stormpath.SDK/Stormpath.SDK/Cache/{ICacheManager.cs => ICacheProvider.cs} (88%) rename Stormpath.SDK/Stormpath.SDK/Cache/{ISynchronousCacheManager.cs => ISynchronousCacheProvider.cs} (89%) rename Stormpath.SDK/Stormpath.SDK/Impl/Cache/{NullCacheManager.cs => NullCacheProvider.cs} (64%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultCacheProviderResolver.cs rename Stormpath.SDK/Stormpath.SDK/Impl/DataStore/{FilterChain => Filters}/DefaultAsynchronousFilter.cs (85%) rename Stormpath.SDK/Stormpath.SDK/Impl/DataStore/{FilterChain => Filters}/DefaultAsynchronousFilterChain.cs (91%) rename Stormpath.SDK/Stormpath.SDK/Impl/DataStore/{FilterChain => Filters}/DefaultSynchronousFilter.cs (87%) rename Stormpath.SDK/Stormpath.SDK/Impl/DataStore/{FilterChain => Filters}/DefaultSynchronousFilterChain.cs (87%) rename Stormpath.SDK/Stormpath.SDK/Impl/DataStore/{FilterChain => Filters}/IAsynchronousFilter.cs (80%) rename Stormpath.SDK/Stormpath.SDK/Impl/DataStore/{FilterChain => Filters}/IAsynchronousFilterChain.cs (95%) rename Stormpath.SDK/Stormpath.SDK/Impl/DataStore/{FilterChain => Filters}/ISynchronousFilter.cs (82%) rename Stormpath.SDK/Stormpath.SDK/Impl/DataStore/{FilterChain => Filters}/ISynchronousFilterChain.cs (85%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ReadCacheFilter.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/WriteCacheFilter.cs diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/StubDataStore.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/StubDataStore.cs index f2006a4e..212da95d 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/StubDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Fakes/StubDataStore.cs @@ -19,6 +19,7 @@ using System.Threading; using System.Threading.Tasks; using Stormpath.SDK.DataStore; +using Stormpath.SDK.Impl.Cache; using Stormpath.SDK.Impl.DataStore; using Stormpath.SDK.Impl.Http; using Stormpath.SDK.Impl.Resource; @@ -39,7 +40,7 @@ public StubDataStore(string resourceJson, string baseHref, ILogger logger = null ? new SDK.Impl.NullLogger() : logger; - this.fakeDataStore = new DefaultDataStore(fakeRequestExecutor.Object, baseHref, new DefaultJsonNetSerializer(), useLogger); + this.fakeDataStore = new DefaultDataStore(fakeRequestExecutor.Object, baseHref, new DefaultJsonNetSerializer(), useLogger, new NullCacheProvider()); } string IInternalDataStore.BaseUrl => this.fakeDataStore.BaseUrl; diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Cache/DefaultCacheResolver_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Cache/DefaultCacheResolver_tests.cs index bdc5153e..084cf151 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Cache/DefaultCacheResolver_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/Cache/DefaultCacheResolver_tests.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using NSubstitute; @@ -33,7 +34,7 @@ public class DefaultCacheResolver_tests [Fact] public void Throws_when_getting_unsupported_synchronous_cache() { - var fakeCacheManager = Substitute.For(); + var fakeCacheManager = Substitute.For(); fakeCacheManager .IsSynchronousSupported .Returns(false); @@ -56,7 +57,7 @@ public void Throws_when_getting_unsupported_synchronous_cache() [Fact] public async Task Throws_when_getting_unsupported_asynchronous_cache() { - var fakeCacheManager = Substitute.For(); + var fakeCacheManager = Substitute.For(); fakeCacheManager .IsAsynchronousSupported .Returns(false); @@ -79,41 +80,41 @@ public async Task Throws_when_getting_unsupported_asynchronous_cache() [Fact] public void Getting_synchronous_cache() { - var fakeCacheManager = Substitute.For(); + var fakeCacheManager = Substitute.For(); fakeCacheManager .IsSynchronousSupported .Returns(true); fakeCacheManager - .GetCache>(Arg.Any()) - .Returns(new NullCache>()); + .GetCache>(Arg.Any()) + .Returns(new NullCache>()); ICacheResolver cacheResolver = new DefaultCacheResolver(fakeCacheManager, Substitute.For()); var cache = cacheResolver.GetCache(); - cache.ShouldBeOfType>>(); + cache.ShouldBeOfType>>(); } [Fact] public async Task Getting_asynchronous_cache() { - var fakeCacheManager = Substitute.For(); + var fakeCacheManager = Substitute.For(); fakeCacheManager .IsAsynchronousSupported .Returns(true); fakeCacheManager - .GetCacheAsync>( + .GetCacheAsync>( Arg.Any(), Arg.Any()) .Returns(async unused_ => { await Task.Yield(); - return (IAsynchronousCache>)new NullCache>(); + return (IAsynchronousCache>)new NullCache>(); }); ICacheResolver cacheResolver = new DefaultCacheResolver(fakeCacheManager, Substitute.For()); var cache = await cacheResolver.GetCacheAsync(CancellationToken.None); - cache.ShouldBeOfType>>(); + cache.ShouldBeOfType>>(); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultCacheProviderResolver_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultCacheProviderResolver_tests.cs new file mode 100644 index 00000000..a876f62e --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultCacheProviderResolver_tests.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using NSubstitute; +using Shouldly; +using Stormpath.SDK.Cache; +using Stormpath.SDK.Impl.Cache; +using Stormpath.SDK.Impl.Client; +using Xunit; + +namespace Stormpath.SDK.Tests.Impl +{ + public class DefaultCacheProviderResolver_tests + { + [Fact] + public void Caching_is_disabled_by_default() + { + var resolver = new DefaultCacheProviderResolver(); + + resolver.UseCache.ShouldBe(false); + resolver.CustomProvider.ShouldBeNull(); + } + + [Fact] + public void Provides_NullCacheProvider_when_cache_is_disabled() + { + var resolver = new DefaultCacheProviderResolver(); + + resolver.UseCache = false; + + var provider = resolver.GetProvider(); + provider.ShouldBeOfType(); + provider.ShouldNotBeNull(); + } + + [Fact] + public void Provides_InMemoryCacheProvider_when_cache_is_enabled() + { + var resolver = new DefaultCacheProviderResolver(); + + resolver.UseCache = true; + + var provider = resolver.GetProvider(); + provider.ShouldBeOfType(); + provider.ShouldNotBeNull(); + } + + [Fact] + public void Provides_custom_provider() + { + var resolver = new DefaultCacheProviderResolver(); + var customProvider = Substitute.For(); + + resolver.UseCache = true; + resolver.CustomProvider = customProvider; + + var provider = resolver.GetProvider(); + provider.ShouldBe(customProvider); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs index 4ea8f54b..c1d9192d 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs @@ -157,8 +157,22 @@ public void Throws_when_no_JSON_serializer_can_be_found() { var client = builder .SetApiKey(fakeKey) - .SetAuthenticationScheme(AuthenticationScheme.SAuthc1) - .SetConnectionTimeout(10) + .Build(); + }); + } + + [Fact] + public void Throws_when_passed_cache_provider_is_null() + { + var fakeKey = Substitute.For(); + fakeKey.IsValid().Returns(true); + + Assert.Throws(() => + { + var client = this.builder + .SetApiKey(fakeKey); + (this.builder as DefaultClientBuilder) + .SetCache(null) .Build(); }); } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FilterChain_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FilterChain_tests.cs index bc901763..020ba319 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FilterChain_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/FilterChain_tests.cs @@ -20,8 +20,9 @@ using System.Threading.Tasks; using NSubstitute; using Shouldly; +using Stormpath.SDK.Account; using Stormpath.SDK.Impl.DataStore; -using Stormpath.SDK.Impl.DataStore.FilterChain; +using Stormpath.SDK.Impl.DataStore.Filters; using Stormpath.SDK.Impl.Http.Support; using Stormpath.SDK.Shared; using Xunit; @@ -34,7 +35,7 @@ public class Sync_tests { internal class CreateInterceptorFilter : ISynchronousFilter { - IResourceDataResult ISynchronousFilter.Execute(IResourceDataRequest request, ISynchronousFilterChain chain, ILogger logger) + IResourceDataResult ISynchronousFilter.Filter(IResourceDataRequest request, ISynchronousFilterChain chain, ILogger logger) { if (request.Action == ResourceAction.Create) { @@ -47,19 +48,19 @@ IResourceDataResult ISynchronousFilter.Execute(IResourceDataRequest request, ISy } else { - return chain.Execute(request, logger); + return chain.Filter(request, logger); } } } internal class DeleteInterceptorFilter : ISynchronousFilter { - IResourceDataResult ISynchronousFilter.Execute(IResourceDataRequest request, ISynchronousFilterChain chain, ILogger logger) + IResourceDataResult ISynchronousFilter.Filter(IResourceDataRequest request, ISynchronousFilterChain chain, ILogger logger) { if (request.Action == ResourceAction.Delete) return new DefaultResourceDataResult(ResourceAction.Delete, null, null, 204, null); else - return chain.Execute(request, logger); + return chain.Filter(request, logger); } } @@ -70,8 +71,8 @@ public void Sync_chain_terminating_on_first() .Add(new CreateInterceptorFilter()) .Add(new DeleteInterceptorFilter()); - var request = new DefaultResourceDataRequest(ResourceAction.Create, new CanonicalUri("http://api.foo.bar")); - var result = filterChain.Execute(request, Substitute.For()); + var request = new DefaultResourceDataRequest(ResourceAction.Create, typeof(IAccount), new CanonicalUri("http://api.foo.bar")); + var result = filterChain.Filter(request, Substitute.For()); result.Action.ShouldBe(ResourceAction.Create); result.Body.ShouldContainKeyAndValue("Foo", "bar"); @@ -84,8 +85,8 @@ public void Sync_chain_terminating_on_second() .Add(new CreateInterceptorFilter()) .Add(new DeleteInterceptorFilter()); - var request = new DefaultResourceDataRequest(ResourceAction.Delete, new CanonicalUri("http://api.foo.bar")); - var result = filterChain.Execute(request, Substitute.For()); + var request = new DefaultResourceDataRequest(ResourceAction.Delete, typeof(IAccount), new CanonicalUri("http://api.foo.bar")); + var result = filterChain.Filter(request, Substitute.For()); result.Action.ShouldBe(ResourceAction.Delete); result.Body.ShouldBe(null); @@ -108,8 +109,8 @@ public void Sync_with_inline_filter() body: new Dictionary() { { "Foo", "bar" } }); })); - var request = new DefaultResourceDataRequest(ResourceAction.Create, new CanonicalUri("http://api.foo.bar")); - var result = finalChain.Execute(request, Substitute.For()); + var request = new DefaultResourceDataRequest(ResourceAction.Create, typeof(IAccount), new CanonicalUri("http://api.foo.bar")); + var result = finalChain.Filter(request, Substitute.For()); result.Action.ShouldBe(ResourceAction.Create); result.Body.ShouldContainKeyAndValue("Foo", "bar"); @@ -120,7 +121,7 @@ public class Async_tests { internal class CreateInterceptorFilter : IAsynchronousFilter { - Task IAsynchronousFilter.ExecuteAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken) + Task IAsynchronousFilter.FilterAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken) { if (request.Action == ResourceAction.Create) { @@ -140,7 +141,7 @@ Task IAsynchronousFilter.ExecuteAsync(IResourceDataRequest internal class DeleteInterceptorFilter : IAsynchronousFilter { - Task IAsynchronousFilter.ExecuteAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken) + Task IAsynchronousFilter.FilterAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken) { if (request.Action == ResourceAction.Delete) { @@ -161,7 +162,7 @@ public async Task Async_chain_terminating_on_first() .Add(new CreateInterceptorFilter()) .Add(new DeleteInterceptorFilter()); - var request = new DefaultResourceDataRequest(ResourceAction.Create, new CanonicalUri("http://api.foo.bar")); + var request = new DefaultResourceDataRequest(ResourceAction.Create, typeof(IAccount), new CanonicalUri("http://api.foo.bar")); var result = await filterChain.ExecuteAsync(request, Substitute.For(), CancellationToken.None); result.Action.ShouldBe(ResourceAction.Create); @@ -175,7 +176,7 @@ public async Task Async_chain_terminating_on_second() .Add(new CreateInterceptorFilter()) .Add(new DeleteInterceptorFilter()); - var request = new DefaultResourceDataRequest(ResourceAction.Delete, new CanonicalUri("http://api.foo.bar")); + var request = new DefaultResourceDataRequest(ResourceAction.Delete, typeof(IAccount), new CanonicalUri("http://api.foo.bar")); var result = await filterChain.ExecuteAsync(request, Substitute.For(), CancellationToken.None); result.Action.ShouldBe(ResourceAction.Delete); @@ -199,7 +200,7 @@ public async Task Async_with_inline_filter() body: new Dictionary() { { "Foo", "bar" } })); })); - var request = new DefaultResourceDataRequest(ResourceAction.Create, new CanonicalUri("http://api.foo.bar")); + var request = new DefaultResourceDataRequest(ResourceAction.Create, typeof(IAccount), new CanonicalUri("http://api.foo.bar")); var result = await finalChain.ExecuteAsync(request, Substitute.For(), CancellationToken.None); result.Action.ShouldBe(ResourceAction.Create); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj index 02a8315f..3602c955 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj @@ -92,6 +92,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK/Cache/IAsynchronousCacheManager.cs b/Stormpath.SDK/Stormpath.SDK/Cache/IAsynchronousCacheProvider.cs similarity index 90% rename from Stormpath.SDK/Stormpath.SDK/Cache/IAsynchronousCacheManager.cs rename to Stormpath.SDK/Stormpath.SDK/Cache/IAsynchronousCacheProvider.cs index 5c69079e..1f29532a 100644 --- a/Stormpath.SDK/Stormpath.SDK/Cache/IAsynchronousCacheManager.cs +++ b/Stormpath.SDK/Stormpath.SDK/Cache/IAsynchronousCacheProvider.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -20,7 +20,7 @@ namespace Stormpath.SDK.Cache { - public interface IAsynchronousCacheManager : ICacheManager + public interface IAsynchronousCacheProvider : ICacheProvider { /// /// Acquires the cache with the specified name. If a cache does not yet exist with that name, diff --git a/Stormpath.SDK/Stormpath.SDK/Cache/ICacheManager.cs b/Stormpath.SDK/Stormpath.SDK/Cache/ICacheProvider.cs similarity index 88% rename from Stormpath.SDK/Stormpath.SDK/Cache/ICacheManager.cs rename to Stormpath.SDK/Stormpath.SDK/Cache/ICacheProvider.cs index e94140f1..fca45840 100644 --- a/Stormpath.SDK/Stormpath.SDK/Cache/ICacheManager.cs +++ b/Stormpath.SDK/Stormpath.SDK/Cache/ICacheProvider.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -17,7 +17,7 @@ namespace Stormpath.SDK.Cache { - public interface ICacheManager + public interface ICacheProvider { bool IsSynchronousSupported { get; } diff --git a/Stormpath.SDK/Stormpath.SDK/Cache/ISynchronousCacheManager.cs b/Stormpath.SDK/Stormpath.SDK/Cache/ISynchronousCacheProvider.cs similarity index 89% rename from Stormpath.SDK/Stormpath.SDK/Cache/ISynchronousCacheManager.cs rename to Stormpath.SDK/Stormpath.SDK/Cache/ISynchronousCacheProvider.cs index 443b6d75..4d1e4b0d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Cache/ISynchronousCacheManager.cs +++ b/Stormpath.SDK/Stormpath.SDK/Cache/ISynchronousCacheProvider.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -17,7 +17,7 @@ namespace Stormpath.SDK.Cache { - public interface ISynchronousCacheManager : ICacheManager + public interface ISynchronousCacheProvider : ICacheProvider { /// /// Acquires the cache with the specified name. If a cache does not yet exist with that name, diff --git a/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs index 71e7479a..a05f79ad 100644 --- a/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Client/IClientBuilder.cs @@ -26,7 +26,6 @@ public interface IClientBuilder IClientBuilder SetApiKey(IClientApiKey apiKey); // TODO IClientBuilder SetProxy(Proxy proxy); - // TODO IClientBuilder SetCacheManager(CacheManager manager); IClientBuilder SetAuthenticationScheme(AuthenticationScheme scheme); IClientBuilder SetConnectionTimeout(int timeout); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/DefaultCacheResolver.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/DefaultCacheResolver.cs index 850d2910..05001c1d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/DefaultCacheResolver.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/DefaultCacheResolver.cs @@ -16,7 +16,7 @@ // using System; -using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Stormpath.SDK.Cache; @@ -25,42 +25,46 @@ namespace Stormpath.SDK.Impl.Cache { internal class DefaultCacheResolver : ICacheResolver { - private readonly ICacheManager cacheManager; - private readonly ISynchronousCacheManager syncCacheManager; - private readonly IAsynchronousCacheManager asyncCacheManager; + private readonly ICacheProvider cacheProvider; + private readonly ISynchronousCacheProvider syncCacheProvider; + private readonly IAsynchronousCacheProvider asyncCacheProvider; private readonly ICacheRegionNameResolver cacheRegionNameResolver; - public DefaultCacheResolver(ICacheManager cacheManager, ICacheRegionNameResolver cacheRegionNameResolver) + bool ICacheResolver.IsSynchronousSupported => this.cacheProvider.IsAsynchronousSupported; + + bool ICacheResolver.IsAsynchronousSupported => this.cacheProvider.IsSynchronousSupported; + + public DefaultCacheResolver(ICacheProvider cacheProvider, ICacheRegionNameResolver cacheRegionNameResolver) { - if (cacheManager == null) - throw new ArgumentNullException(nameof(cacheManager)); + if (cacheProvider == null) + throw new ArgumentNullException(nameof(cacheProvider)); if (cacheRegionNameResolver == null) throw new ArgumentNullException(nameof(cacheRegionNameResolver)); - this.cacheManager = cacheManager; - this.syncCacheManager = cacheManager as ISynchronousCacheManager; - this.asyncCacheManager = cacheManager as IAsynchronousCacheManager; + this.cacheProvider = cacheProvider; + this.syncCacheProvider = cacheProvider as ISynchronousCacheProvider; + this.asyncCacheProvider = cacheProvider as IAsynchronousCacheProvider; this.cacheRegionNameResolver = cacheRegionNameResolver; } - ISynchronousCache> ICacheResolver.GetCache() + ISynchronousCache> ICacheResolver.GetCache() { - if (!this.cacheManager.IsSynchronousSupported || this.syncCacheManager == null) - throw new ApplicationException($"A synchronous caching path is not supported in {this.cacheManager.GetType().Name}"); + if (!this.cacheProvider.IsSynchronousSupported || this.syncCacheProvider == null) + throw new ApplicationException($"A synchronous caching path is not supported in {this.cacheProvider.GetType().Name}"); var cacheRegionName = this.cacheRegionNameResolver.GetCacheRegionName(); - return this.syncCacheManager.GetCache>(cacheRegionName); + return this.syncCacheProvider.GetCache>(cacheRegionName); } - Task>> ICacheResolver.GetCacheAsync(CancellationToken cancellationToken) + Task>> ICacheResolver.GetCacheAsync(CancellationToken cancellationToken) { - if (!this.cacheManager.IsAsynchronousSupported || this.asyncCacheManager == null) - throw new ApplicationException($"An asynchronous caching path is not supported in {this.cacheManager.GetType().Name}"); + if (!this.cacheProvider.IsAsynchronousSupported || this.asyncCacheProvider == null) + throw new ApplicationException($"An asynchronous caching path is not supported in {this.cacheProvider.GetType().Name}"); var cacheRegionName = this.cacheRegionNameResolver.GetCacheRegionName(); - return this.asyncCacheManager.GetCacheAsync>(cacheRegionName, cancellationToken); + return this.asyncCacheProvider.GetCacheAsync>(cacheRegionName, cancellationToken); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/ICacheResolver.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/ICacheResolver.cs index ff2b5ba0..54777984 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/ICacheResolver.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/ICacheResolver.cs @@ -15,7 +15,7 @@ // limitations under the License. // -using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Stormpath.SDK.Cache; @@ -25,10 +25,14 @@ namespace Stormpath.SDK.Impl.Cache { internal interface ICacheResolver { - ISynchronousCache> GetCache() + bool IsSynchronousSupported { get; } + + bool IsAsynchronousSupported { get; } + + ISynchronousCache> GetCache() where T : IResource; - Task>> GetCacheAsync(CancellationToken cancellationToken) + Task>> GetCacheAsync(CancellationToken cancellationToken) where T : IResource; } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCache.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCache.cs index 9756414e..78d0c8b4 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCache.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCache.cs @@ -31,7 +31,7 @@ internal sealed class InMemoryCache : ISynchronousCache, IAsynchrono private readonly TimeSpan? timeToLive; private readonly TimeSpan? timeToIdle; - private InMemoryCacheProvider cacheProvider; + private InMemoryCacheManager cacheManager; private bool alreadyDisposed = false; private long accessCount; @@ -52,7 +52,7 @@ public InMemoryCache(string region, TimeSpan? timeToLive, TimeSpan? timeToIdle) if (timeToIdle.HasValue && timeToIdle.Value.TotalMilliseconds <= 0) throw new ArgumentException("TTI duration must be greater than zero.", nameof(timeToIdle)); - this.cacheProvider = new InMemoryCacheProvider(); + this.cacheManager = new InMemoryCacheManager(); this.region = region; this.timeToLive = timeToLive; this.timeToIdle = timeToIdle; @@ -74,7 +74,7 @@ private string CreateCompositeKey(K key) /// /// The number of items stored in all regions of the cache. /// - public long TotalSize => this.cacheProvider.Count; + public long TotalSize => this.cacheManager.Count; public TimeSpan? TimeToLive => this.timeToLive; @@ -88,8 +88,8 @@ private string CreateCompositeKey(K key) public void Clear() { - this.cacheProvider.Dispose(); - this.cacheProvider = new InMemoryCacheProvider(); + this.cacheManager.Dispose(); + this.cacheManager = new InMemoryCacheManager(); } V ISynchronousCache.Get(K key) @@ -97,7 +97,7 @@ V ISynchronousCache.Get(K key) Interlocked.Increment(ref this.accessCount); var compositeKey = this.CreateCompositeKey(key); - V value = this.cacheProvider.Get(compositeKey); + V value = this.cacheManager.Get(compositeKey); if (value == null) Interlocked.Increment(ref this.missCount); @@ -117,7 +117,7 @@ V ISynchronousCache.Put(K key, V value) ? this.timeToIdle.Value : ObjectCache.NoSlidingExpiration; - return this.cacheProvider.Put(compositeKey, value, absoluteExpiration, slidingExpiration); + return this.cacheManager.Put(compositeKey, value, absoluteExpiration, slidingExpiration); } V ISynchronousCache.Remove(K key) @@ -125,7 +125,7 @@ V ISynchronousCache.Remove(K key) Interlocked.Increment(ref this.accessCount); var compositeKey = this.CreateCompositeKey(key); - var value = this.cacheProvider.Remove(compositeKey); + var value = this.cacheManager.Remove(compositeKey); if (value == null) Interlocked.Increment(ref this.missCount); @@ -186,7 +186,7 @@ private void Dispose(bool disposing) { if (disposing) { - this.cacheProvider.Dispose(); + this.cacheManager.Dispose(); } this.alreadyDisposed = true; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCacheManager.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCacheManager.cs index 4f36c5b4..9376c72d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCacheManager.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCacheManager.cs @@ -16,26 +16,89 @@ // using System; -using System.Threading; -using System.Threading.Tasks; -using Stormpath.SDK.Cache; +using System.Runtime.Caching; namespace Stormpath.SDK.Impl.Cache { - internal class InMemoryCacheManager : ISynchronousCacheManager, IAsynchronousCacheManager + /// + /// A wrapper around that allows for both absolute and sliding expirations. (This separates implementation details from .) + /// + /// The type of vaules stored in the cache. + internal sealed class InMemoryCacheManager : IDisposable + where V : class { - bool ICacheManager.IsAsynchronousSupported => false; // todo + private readonly MemoryCache memoryCache; + private bool alreadyDisposed = false; - bool ICacheManager.IsSynchronousSupported => false; + public InMemoryCacheManager() + { + this.memoryCache = new MemoryCache("StormpathSDK"); + } + + private static string CreateAbsoluteTokenKey(string key) + { + return $"{key}-absoluteToken"; + } - ISynchronousCache ISynchronousCacheManager.GetCache(string name) + public V Get(string key) { - throw new NotImplementedException(); + var absoluteTokenKey = CreateAbsoluteTokenKey(key); + var tokenAndItem = this.memoryCache.GetValues(new string[] { absoluteTokenKey, key }); + + bool itemHasNotExpired = + tokenAndItem != null && + (tokenAndItem.ContainsKey(absoluteTokenKey) && tokenAndItem[absoluteTokenKey] != null) && + (tokenAndItem.ContainsKey(key) && tokenAndItem[key] != null); + + if (itemHasNotExpired) + return (V)tokenAndItem[key]; + else + return null; + } + + public V Put(string key, V value, DateTimeOffset absoluteExpiration, TimeSpan slidingExpiration) + { + var absoluteTokenKey = CreateAbsoluteTokenKey(key); + bool absoluteTokenInserted = this.memoryCache.Add(absoluteTokenKey, new object(), absoluteExpiration); + + // Create a monitor to link the two items + var monitor = this.memoryCache.CreateCacheEntryChangeMonitor(new string[] { absoluteTokenKey }); + + var mainItemPolicy = new CacheItemPolicy(); + mainItemPolicy.SlidingExpiration = slidingExpiration; + mainItemPolicy.ChangeMonitors.Add(monitor); + bool mainItemInserted = this.memoryCache.Add(key, value, mainItemPolicy); + + if (absoluteTokenInserted && mainItemInserted) + return value; + else + return null; + } + + public V Remove(string key) + { + this.memoryCache.Remove(CreateAbsoluteTokenKey(key)); + return (V)this.memoryCache.Remove(key); + } + + public long Count => this.memoryCache.GetCount() / 2; + + private void Dispose(bool disposing) + { + if (!this.alreadyDisposed) + { + if (disposing) + { + this.memoryCache.Dispose(); + } + + this.alreadyDisposed = true; + } } - Task> IAsynchronousCacheManager.GetCacheAsync(string name, CancellationToken cancellationToken) + public void Dispose() { - throw new NotImplementedException(); + this.Dispose(true); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCacheProvider.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCacheProvider.cs index d5fbbd83..8a087d9e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCacheProvider.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/InMemoryCacheProvider.cs @@ -16,89 +16,26 @@ // using System; -using System.Runtime.Caching; +using System.Threading; +using System.Threading.Tasks; +using Stormpath.SDK.Cache; namespace Stormpath.SDK.Impl.Cache { - /// - /// A wrapper around that allows for both absolute and sliding expirations. (This separates implementation details from .) - /// - /// The type of vaules stored in the cache. - internal sealed class InMemoryCacheProvider : IDisposable - where V : class + internal class InMemoryCacheProvider : ISynchronousCacheProvider, IAsynchronousCacheProvider { - private readonly MemoryCache memoryCache; - private bool alreadyDisposed = false; + bool ICacheProvider.IsAsynchronousSupported => false; // todo - public InMemoryCacheProvider() - { - this.memoryCache = new MemoryCache("StormpathSDK"); - } - - private static string CreateAbsoluteTokenKey(string key) - { - return $"{key}-absoluteToken"; - } + bool ICacheProvider.IsSynchronousSupported => false; - public V Get(string key) + ISynchronousCache ISynchronousCacheProvider.GetCache(string name) { - var absoluteTokenKey = CreateAbsoluteTokenKey(key); - var tokenAndItem = this.memoryCache.GetValues(new string[] { absoluteTokenKey, key }); - - bool itemHasNotExpired = - tokenAndItem != null && - (tokenAndItem.ContainsKey(absoluteTokenKey) && tokenAndItem[absoluteTokenKey] != null) && - (tokenAndItem.ContainsKey(key) && tokenAndItem[key] != null); - - if (itemHasNotExpired) - return (V)tokenAndItem[key]; - else - return null; - } - - public V Put(string key, V value, DateTimeOffset absoluteExpiration, TimeSpan slidingExpiration) - { - var absoluteTokenKey = CreateAbsoluteTokenKey(key); - bool absoluteTokenInserted = this.memoryCache.Add(absoluteTokenKey, new object(), absoluteExpiration); - - // Create a monitor to link the two items - var monitor = this.memoryCache.CreateCacheEntryChangeMonitor(new string[] { absoluteTokenKey }); - - var mainItemPolicy = new CacheItemPolicy(); - mainItemPolicy.SlidingExpiration = slidingExpiration; - mainItemPolicy.ChangeMonitors.Add(monitor); - bool mainItemInserted = this.memoryCache.Add(key, value, mainItemPolicy); - - if (absoluteTokenInserted && mainItemInserted) - return value; - else - return null; - } - - public V Remove(string key) - { - this.memoryCache.Remove(CreateAbsoluteTokenKey(key)); - return (V)this.memoryCache.Remove(key); - } - - public long Count => this.memoryCache.GetCount() / 2; - - private void Dispose(bool disposing) - { - if (!this.alreadyDisposed) - { - if (disposing) - { - this.memoryCache.Dispose(); - } - - this.alreadyDisposed = true; - } + throw new NotImplementedException(); } - public void Dispose() + Task> IAsynchronousCacheProvider.GetCacheAsync(string name, CancellationToken cancellationToken) { - this.Dispose(true); + throw new NotImplementedException(); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/NullCacheManager.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/NullCacheProvider.cs similarity index 64% rename from Stormpath.SDK/Stormpath.SDK/Impl/Cache/NullCacheManager.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/Cache/NullCacheProvider.cs index 3492876e..4d29627f 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Cache/NullCacheManager.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Cache/NullCacheProvider.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2015 Stormpath, Inc. // // @@ -21,18 +21,18 @@ namespace Stormpath.SDK.Impl.Cache { - internal sealed class NullCacheManager : ISynchronousCacheManager, IAsynchronousCacheManager + internal sealed class NullCacheProvider : ISynchronousCacheProvider, IAsynchronousCacheProvider { - bool ICacheManager.IsAsynchronousSupported => true; + bool ICacheProvider.IsAsynchronousSupported => true; - bool ICacheManager.IsSynchronousSupported => true; + bool ICacheProvider.IsSynchronousSupported => true; - ISynchronousCache ISynchronousCacheManager.GetCache(string name) + ISynchronousCache ISynchronousCacheProvider.GetCache(string name) { return new NullCache(); } - Task> IAsynchronousCacheManager.GetCacheAsync(string name, CancellationToken cancellationToken) + Task> IAsynchronousCacheProvider.GetCacheAsync(string name, CancellationToken cancellationToken) { return Task.FromResult>(new NullCache()); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultCacheProviderResolver.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultCacheProviderResolver.cs new file mode 100644 index 00000000..3a251825 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultCacheProviderResolver.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Stormpath.SDK.Cache; +using Stormpath.SDK.Impl.Cache; + +namespace Stormpath.SDK.Impl.Client +{ + internal class DefaultCacheProviderResolver + { + public bool UseCache { get; set; } = false; + + public ICacheProvider CustomProvider { get; set; } = null; + + public ICacheProvider GetProvider() + { + if (!this.UseCache) + return new NullCacheProvider(); + + if (this.CustomProvider == null) + return new InMemoryCacheProvider(); + + return this.CustomProvider; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs index 3e0b01cf..55aab4fb 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClient.cs @@ -21,6 +21,7 @@ using Stormpath.SDK.Account; using Stormpath.SDK.Api; using Stormpath.SDK.Application; +using Stormpath.SDK.Cache; using Stormpath.SDK.Client; using Stormpath.SDK.DataStore; using Stormpath.SDK.Directory; @@ -49,7 +50,7 @@ internal sealed class DefaultClient : IClient private string currentTenantHref; - public DefaultClient(IClientApiKey apiKey, string baseUrl, AuthenticationScheme authenticationScheme, int connectionTimeout, IJsonSerializer serializer, ILogger logger) + public DefaultClient(IClientApiKey apiKey, string baseUrl, AuthenticationScheme authenticationScheme, int connectionTimeout, IJsonSerializer serializer, ILogger logger, ICacheProvider cacheProvider) { if (apiKey == null || !apiKey.IsValid()) throw new ArgumentException("API Key is not valid."); @@ -72,7 +73,7 @@ public DefaultClient(IClientApiKey apiKey, string baseUrl, AuthenticationScheme : authenticationScheme; var requestExecutor = factory.CreateRequestExecutor(apiKey, authenticationScheme, connectionTimeout, this.logger); - this.dataStore = factory.CreateDataStore(requestExecutor, baseUrl, this.serializer, this.logger); + this.dataStore = factory.CreateDataStore(requestExecutor, baseUrl, this.serializer, this.logger, cacheProvider); } private IClient AsInterface => this; @@ -84,20 +85,11 @@ AuthenticationScheme IClient.AuthenticationScheme get { return this.dataStore.RequestExecutor.AuthenticationScheme; } } - string IClient.BaseUrl - { - get { return this.dataStore.BaseUrl; } - } + string IClient.BaseUrl => this.dataStore.BaseUrl; - int IClient.ConnectionTimeout - { - get { return this.dataStore.RequestExecutor.ConnectionTimeout; } - } + int IClient.ConnectionTimeout => this.dataStore.RequestExecutor.ConnectionTimeout; - T IDataStore.Instantiate() - { - return this.dataStore.Instantiate(); - } + T IDataStore.Instantiate() => this.dataStore.Instantiate(); async Task IClient.GetCurrentTenantAsync(CancellationToken cancellationToken) { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs index 3bd5bd81..5762537e 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs @@ -17,6 +17,7 @@ using System; using Stormpath.SDK.Api; +using Stormpath.SDK.Cache; using Stormpath.SDK.Client; using Stormpath.SDK.Impl.Serialization; using Stormpath.SDK.Serialization; @@ -27,6 +28,7 @@ namespace Stormpath.SDK.Impl.Client internal sealed class DefaultClientBuilder : IClientBuilder { private readonly IJsonSerializerLoader serializerLoader; + private readonly DefaultCacheProviderResolver cacheProviderResolver; private string baseUrl = DefaultClient.DefaultBaseUrl; private int connectionTimeout = DefaultClient.DefaultConnectionTimeout; @@ -43,6 +45,7 @@ public DefaultClientBuilder() internal DefaultClientBuilder(IJsonSerializerLoader serializerLoader) { this.serializerLoader = serializerLoader; + this.cacheProviderResolver = new DefaultCacheProviderResolver(); } IClientBuilder IClientBuilder.SetApiKey(IClientApiKey apiKey) @@ -90,6 +93,24 @@ IClientBuilder IClientBuilder.SetLogger(ILogger logger) return this; } + internal IClientBuilder SetCache(bool cacheEnabled) + { + this.cacheProviderResolver.UseCache = cacheEnabled; + + return this; + } + + internal IClientBuilder SetCache(ICacheProvider cacheProvider) + { + if (cacheProvider == null) + throw new ArgumentNullException(nameof(cacheProvider)); + + this.cacheProviderResolver.UseCache = true; + this.cacheProviderResolver.CustomProvider = cacheProvider; + + return this; + } + IClient IClientBuilder.Build() { if (this.apiKey == null || !this.apiKey.IsValid()) @@ -103,7 +124,9 @@ IClient IClientBuilder.Build() if (!isValidSerializer) throw new ApplicationException("Could not find a valid JSON serializer. Include Stormpath.SDK.JsonNetSerializer.dll in the application path."); - return new DefaultClient(this.apiKey, this.baseUrl, this.authenticationScheme, this.connectionTimeout, useSerializer, this.logger); + var useCacheProvider = this.cacheProviderResolver.GetProvider(); + + return new DefaultClient(this.apiKey, this.baseUrl, this.authenticationScheme, this.connectionTimeout, useSerializer, this.logger, useCacheProvider); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs index ad87002e..24f45964 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs @@ -24,7 +24,7 @@ using Stormpath.SDK.DataStore; using Stormpath.SDK.Error; using Stormpath.SDK.Impl.Cache; -using Stormpath.SDK.Impl.DataStore.FilterChain; +using Stormpath.SDK.Impl.DataStore.Filters; using Stormpath.SDK.Impl.Error; using Stormpath.SDK.Impl.Http; using Stormpath.SDK.Impl.Http.Support; @@ -42,7 +42,7 @@ internal sealed class DefaultDataStore : IInternalDataStore, IDisposable private readonly string baseUrl; private readonly IRequestExecutor requestExecutor; - private readonly ICacheManager cacheManager; + private readonly ICacheProvider cacheProvider; private readonly ICacheResolver cacheResolver; private readonly JsonSerializationProvider serializer; private readonly IResourceFactory resourceFactory; @@ -59,16 +59,7 @@ internal sealed class DefaultDataStore : IInternalDataStore, IDisposable string IInternalDataStore.BaseUrl => this.baseUrl; -#pragma warning disable SA1124 // Do not use regions - #region Constructor and setup -#pragma warning restore SA1124 // Do not use regions - - internal DefaultDataStore(IRequestExecutor requestExecutor, string baseUrl, IJsonSerializer serializer, ILogger logger) - : this(requestExecutor, baseUrl, serializer, logger, new NullCacheManager()) - { - } - - internal DefaultDataStore(IRequestExecutor requestExecutor, string baseUrl, IJsonSerializer serializer, ILogger logger, ICacheManager cacheManager) + internal DefaultDataStore(IRequestExecutor requestExecutor, string baseUrl, IJsonSerializer serializer, ILogger logger, ICacheProvider cacheProvider) { if (requestExecutor == null) throw new ArgumentNullException(nameof(requestExecutor)); @@ -76,13 +67,13 @@ internal DefaultDataStore(IRequestExecutor requestExecutor, string baseUrl, IJso throw new ArgumentNullException(nameof(baseUrl)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - if (cacheManager == null) - throw new ArgumentNullException(nameof(cacheManager), "Use NullCacheManager if you wish to turn off caching."); + if (cacheProvider == null) + throw new ArgumentNullException(nameof(cacheProvider), "Use NullCacheManager if you wish to turn off caching."); this.baseUrl = baseUrl; this.requestExecutor = requestExecutor; - this.cacheManager = cacheManager; - this.cacheResolver = new DefaultCacheResolver(this.cacheManager, new DefaultCacheRegionNameResolver()); + this.cacheProvider = cacheProvider; + this.cacheResolver = new DefaultCacheResolver(cacheProvider, new DefaultCacheRegionNameResolver()); this.serializer = new JsonSerializationProvider(serializer); this.resourceFactory = new DefaultResourceFactory(this); @@ -95,10 +86,17 @@ internal DefaultDataStore(IRequestExecutor requestExecutor, string baseUrl, IJso this.defaultSyncFilters = this.BuildDefaultSyncFilterChain(); } + // *** Helper methods *** private IAsynchronousFilterChain BuildDefaultAsyncFilterChain() { var asyncFilterChain = new DefaultAsynchronousFilterChain(); + if (this.IsCachingEnabled()) + { + asyncFilterChain.Add(new ReadCacheFilter(this.baseUrl, this.cacheResolver)); + asyncFilterChain.Add(new WriteCacheFilter(this.cacheResolver)); + } + return asyncFilterChain; } @@ -106,20 +104,20 @@ private ISynchronousFilterChain BuildDefaultSyncFilterChain() { var syncFilterChain = new DefaultSynchronousFilterChain(); + if (this.IsCachingEnabled()) + { + syncFilterChain.Add(new ReadCacheFilter(this.baseUrl, this.cacheResolver)); + syncFilterChain.Add(new WriteCacheFilter(this.cacheResolver)); + } + return syncFilterChain; } - #endregion - T IDataStore.Instantiate() { return this.resourceFactory.Create(); } -#pragma warning disable SA1124 // Do not use regions - #region Helper methods -#pragma warning restore SA1124 // Do not use regions - private void ApplyDefaultRequestHeaders(IHttpRequest request) { request.Headers.Accept = "application/json"; @@ -139,7 +137,7 @@ private IDictionary GetBody(IHttpResponse response) private bool IsCachingEnabled() { - return this.cacheManager != null && !(this.cacheManager is NullCacheManager); + return this.cacheProvider != null && !(this.cacheProvider is NullCacheProvider); } private QueryString CreateQueryStringFromCreationOptions(ICreationOptions options) @@ -174,8 +172,7 @@ private IHttpResponse HandleResponseOrError(IHttpResponse response) return response; } - #endregion - + // DataStore methods async Task IDataStore.GetResourceAsync(string resourcePath, CancellationToken cancellationToken) { var canonicalUri = new CanonicalUri(this.uriQualifier.EnsureFullyQualified(resourcePath)); @@ -192,7 +189,7 @@ async Task IDataStore.GetResourceAsync(string resourcePath, CancellationTo return new DefaultResourceDataResult(req.Action, typeof(T), req.Uri, response.HttpStatus, body); })); - var request = new DefaultResourceDataRequest(ResourceAction.Read, canonicalUri); + var request = new DefaultResourceDataRequest(ResourceAction.Read, typeof(T), canonicalUri); var result = await chain.ExecuteAsync(request, this.logger, cancellationToken).ConfigureAwait(false); return this.resourceFactory.Create(result.Body); @@ -214,8 +211,8 @@ T IInternalDataStore.GetResource(string resourcePath) return new DefaultResourceDataResult(req.Action, typeof(T), req.Uri, response.HttpStatus, body); })); - var request = new DefaultResourceDataRequest(ResourceAction.Read, canonicalUri); - var result = chain.Execute(request, this.logger); + var request = new DefaultResourceDataRequest(ResourceAction.Read, typeof(T), canonicalUri); + var result = chain.Filter(request, this.logger); return this.resourceFactory.Create(result.Body); } @@ -380,7 +377,7 @@ private async Task SaveCoreAsync(T resource, string hre var requestAction = create ? ResourceAction.Create : ResourceAction.Update; - var request = new DefaultResourceDataRequest(requestAction, canonicalUri, propertiesMap); + var request = new DefaultResourceDataRequest(requestAction, typeof(T), canonicalUri, propertiesMap); var result = await chain.ExecuteAsync(request, this.logger, cancellationToken).ConfigureAwait(false); return this.resourceFactory.Create(result.Body); @@ -425,9 +422,9 @@ private TReturned SaveCore(T resource, string href, QueryString qu var requestAction = create ? ResourceAction.Create : ResourceAction.Update; - var request = new DefaultResourceDataRequest(requestAction, canonicalUri, propertiesMap); + var request = new DefaultResourceDataRequest(requestAction, typeof(T), canonicalUri, propertiesMap); - var result = chain.Execute(request, this.logger); + var result = chain.Filter(request, this.logger); return this.resourceFactory.Create(result.Body); } @@ -453,7 +450,7 @@ private async Task DeleteCoreAsync(T resource, CancellationToken cancel return new DefaultResourceDataResult(req.Action, typeof(T), req.Uri, response.HttpStatus, body: null); })); - var request = new DefaultResourceDataRequest(ResourceAction.Delete, uri); + var request = new DefaultResourceDataRequest(ResourceAction.Delete, typeof(T), uri); var result = await chain.ExecuteAsync(request, this.logger, cancellationToken).ConfigureAwait(false); bool successfullyDeleted = result.HttpStatus == 204; @@ -482,8 +479,8 @@ private bool DeleteCore(T resource) return new DefaultResourceDataResult(req.Action, typeof(T), req.Uri, response.HttpStatus, body: null); })); - var request = new DefaultResourceDataRequest(ResourceAction.Delete, uri); - var result = chain.Execute(request, this.logger); + var request = new DefaultResourceDataRequest(ResourceAction.Delete, typeof(T), uri); + var result = chain.Filter(request, this.logger); bool successfullyDeleted = result.HttpStatus == 204; return successfullyDeleted; diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceDataRequest.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceDataRequest.cs index 02f6ccf7..21bbdf2d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceDataRequest.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultResourceDataRequest.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System; using System.Collections.Generic; using Stormpath.SDK.Impl.Http.Support; @@ -23,18 +24,20 @@ namespace Stormpath.SDK.Impl.DataStore internal sealed class DefaultResourceDataRequest : IResourceDataRequest { private readonly ResourceAction action; + private readonly Type resourceType; private readonly CanonicalUri uri; private readonly IDictionary properties; - public DefaultResourceDataRequest(ResourceAction action, CanonicalUri uri) - : this(action, uri, null) + public DefaultResourceDataRequest(ResourceAction action, Type resourceType, CanonicalUri uri) + : this(action, resourceType, uri, null) { } - public DefaultResourceDataRequest(ResourceAction action, CanonicalUri uri, IDictionary properties) + public DefaultResourceDataRequest(ResourceAction action, Type resourceType, CanonicalUri uri, IDictionary properties) { this.action = action; this.uri = uri; + this.resourceType = resourceType; this.properties = properties; } @@ -42,6 +45,8 @@ public DefaultResourceDataRequest(ResourceAction action, CanonicalUri uri, IDict IDictionary IResourceDataRequest.Properties => this.properties; + Type IResourceDataRequest.ResourceType => this.resourceType; + CanonicalUri IResourceDataRequest.Uri => this.uri; } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/DefaultAsynchronousFilter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/DefaultAsynchronousFilter.cs similarity index 85% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/DefaultAsynchronousFilter.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/DefaultAsynchronousFilter.cs index bb9e4013..e26fc9ef 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/DefaultAsynchronousFilter.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/DefaultAsynchronousFilter.cs @@ -20,7 +20,7 @@ using System.Threading.Tasks; using Stormpath.SDK.Shared; -namespace Stormpath.SDK.Impl.DataStore.FilterChain +namespace Stormpath.SDK.Impl.DataStore.Filters { internal sealed class DefaultAsynchronousFilter : IAsynchronousFilter { @@ -31,7 +31,7 @@ public DefaultAsynchronousFilter(Func IAsynchronousFilter.ExecuteAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken) + Task IAsynchronousFilter.FilterAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken) { return this.filterFunc(request, chain, logger, cancellationToken); } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/DefaultAsynchronousFilterChain.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/DefaultAsynchronousFilterChain.cs similarity index 91% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/DefaultAsynchronousFilterChain.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/DefaultAsynchronousFilterChain.cs index 1d179657..405c367a 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/DefaultAsynchronousFilterChain.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/DefaultAsynchronousFilterChain.cs @@ -22,7 +22,7 @@ using System.Threading.Tasks; using Stormpath.SDK.Shared; -namespace Stormpath.SDK.Impl.DataStore.FilterChain +namespace Stormpath.SDK.Impl.DataStore.Filters { internal sealed class DefaultAsynchronousFilterChain : IAsynchronousFilterChain { @@ -59,7 +59,7 @@ Task IAsynchronousFilterChain.ExecuteAsync(IResourceDataReq if (this.filters.Count == 1) { - return this.filters.Single().ExecuteAsync( + return this.filters.Single().FilterAsync( request, chain: null, logger: logger, @@ -67,7 +67,7 @@ Task IAsynchronousFilterChain.ExecuteAsync(IResourceDataReq } var remainingChain = new DefaultAsynchronousFilterChain(this.filters.Skip(1).ToList()); - return this.filters.First().ExecuteAsync(request, remainingChain, logger, cancellationToken); + return this.filters.First().FilterAsync(request, remainingChain, logger, cancellationToken); } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/DefaultSynchronousFilter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/DefaultSynchronousFilter.cs similarity index 87% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/DefaultSynchronousFilter.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/DefaultSynchronousFilter.cs index 5860e732..4266a43a 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/DefaultSynchronousFilter.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/DefaultSynchronousFilter.cs @@ -18,7 +18,7 @@ using System; using Stormpath.SDK.Shared; -namespace Stormpath.SDK.Impl.DataStore.FilterChain +namespace Stormpath.SDK.Impl.DataStore.Filters { internal sealed class DefaultSynchronousFilter : ISynchronousFilter { @@ -29,7 +29,7 @@ public DefaultSynchronousFilter(Func ExecuteAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken); + Task FilterAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/IAsynchronousFilterChain.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/IAsynchronousFilterChain.cs similarity index 95% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/IAsynchronousFilterChain.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/IAsynchronousFilterChain.cs index e065ea68..c36a1ff6 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/IAsynchronousFilterChain.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/IAsynchronousFilterChain.cs @@ -19,7 +19,7 @@ using System.Threading.Tasks; using Stormpath.SDK.Shared; -namespace Stormpath.SDK.Impl.DataStore.FilterChain +namespace Stormpath.SDK.Impl.DataStore.Filters { internal interface IAsynchronousFilterChain { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/ISynchronousFilter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ISynchronousFilter.cs similarity index 82% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/ISynchronousFilter.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ISynchronousFilter.cs index fd340015..0f7c0e12 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/ISynchronousFilter.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ISynchronousFilter.cs @@ -17,10 +17,10 @@ using Stormpath.SDK.Shared; -namespace Stormpath.SDK.Impl.DataStore.FilterChain +namespace Stormpath.SDK.Impl.DataStore.Filters { internal interface ISynchronousFilter { - IResourceDataResult Execute(IResourceDataRequest request, ISynchronousFilterChain chain, ILogger logger); + IResourceDataResult Filter(IResourceDataRequest request, ISynchronousFilterChain chain, ILogger logger); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/ISynchronousFilterChain.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ISynchronousFilterChain.cs similarity index 85% rename from Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/ISynchronousFilterChain.cs rename to Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ISynchronousFilterChain.cs index d1ddffaf..558ae020 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/FilterChain/ISynchronousFilterChain.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ISynchronousFilterChain.cs @@ -17,10 +17,10 @@ using Stormpath.SDK.Shared; -namespace Stormpath.SDK.Impl.DataStore.FilterChain +namespace Stormpath.SDK.Impl.DataStore.Filters { internal interface ISynchronousFilterChain { - IResourceDataResult Execute(IResourceDataRequest request, ILogger logger); + IResourceDataResult Filter(IResourceDataRequest request, ILogger logger); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ReadCacheFilter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ReadCacheFilter.cs new file mode 100644 index 00000000..ee362b6c --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/ReadCacheFilter.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Stormpath.SDK.Impl.Auth; +using Stormpath.SDK.Impl.Cache; +using Stormpath.SDK.Shared; + +namespace Stormpath.SDK.Impl.DataStore.Filters +{ + internal sealed class ReadCacheFilter : IAsynchronousFilter, ISynchronousFilter + { + private readonly string baseUrl; + private readonly ICacheResolver cacheResolver; + + public ReadCacheFilter(string baseUrl, ICacheResolver cacheResolver) + { + if (string.IsNullOrEmpty(baseUrl)) + throw new ArgumentNullException(nameof(baseUrl)); + if (cacheResolver == null) + throw new ArgumentNullException(nameof(cacheResolver)); + + this.baseUrl = baseUrl; + this.cacheResolver = cacheResolver; + + // TODO + throw new NotImplementedException(); + } + + IResourceDataResult ISynchronousFilter.Filter(IResourceDataRequest request, ISynchronousFilterChain chain, ILogger logger) + { + if (this.cacheResolver.IsSynchronousSupported && this.IsCacheRetrievalEnabled(request)) + { + var result = this.GetCachedResourceData(request); + + if (result != null) + return result; // short-circuit the remainder of the filter chain + } + + return chain.Filter(request, logger); // cache miss + } + + Task IAsynchronousFilter.FilterAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + private IResourceDataResult GetCachedResourceData(IResourceDataRequest request) + { + IDictionary data = null; + + var cacheKey = this.GetCacheKey(request); + + if (data == null) + return null; + + return new DefaultResourceDataResult(request.Action, request.ResourceType, request.Uri, 200, data); + } + + private string GetCacheKey(IResourceDataRequest request) + { + throw new NotImplementedException(); + } + + private bool IsCacheRetrievalEnabled(IResourceDataRequest request) + { + bool isRead = request.Action == ResourceAction.Read; + bool isLoginAttempt = request.ResourceType is ILoginAttempt; + + return + isRead && + !isLoginAttempt; + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/WriteCacheFilter.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/WriteCacheFilter.cs new file mode 100644 index 00000000..fd1077ac --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/Filters/WriteCacheFilter.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2015 Stormpath, Inc. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using Stormpath.SDK.Impl.Cache; +using Stormpath.SDK.Shared; + +namespace Stormpath.SDK.Impl.DataStore.Filters +{ + internal sealed class WriteCacheFilter : IAsynchronousFilter, ISynchronousFilter + { + private readonly ICacheResolver cacheResolver; + + public WriteCacheFilter(ICacheResolver cacheResolver) + { + this.cacheResolver = cacheResolver; + } + + IResourceDataResult ISynchronousFilter.Filter(IResourceDataRequest request, ISynchronousFilterChain chain, ILogger logger) + { + // TODO + throw new NotImplementedException(); + } + + Task IAsynchronousFilter.FilterAsync(IResourceDataRequest request, IAsynchronousFilterChain chain, ILogger logger, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceDataRequest.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceDataRequest.cs index 439c81e1..ef640797 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceDataRequest.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/IResourceDataRequest.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System; using System.Collections.Generic; using Stormpath.SDK.Impl.Http.Support; @@ -26,6 +27,8 @@ internal interface IResourceDataRequest CanonicalUri Uri { get; } + Type ResourceType { get; } + IDictionary Properties { get; } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/InternalFactory.cs b/Stormpath.SDK/Stormpath.SDK/Impl/InternalFactory.cs index 123b4baf..3973f3d7 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/InternalFactory.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/InternalFactory.cs @@ -16,6 +16,7 @@ // using Stormpath.SDK.Api; +using Stormpath.SDK.Cache; using Stormpath.SDK.Client; using Stormpath.SDK.Impl.Auth; using Stormpath.SDK.Impl.DataStore; @@ -27,9 +28,9 @@ namespace Stormpath.SDK.Impl { internal sealed class InternalFactory { - public IInternalDataStore CreateDataStore(IRequestExecutor requestExecutor, string baseUrl, IJsonSerializer serializer, ILogger logger) + public IInternalDataStore CreateDataStore(IRequestExecutor requestExecutor, string baseUrl, IJsonSerializer serializer, ILogger logger, ICacheProvider cacheManager) { - return new DefaultDataStore(requestExecutor, baseUrl, serializer, logger); + return new DefaultDataStore(requestExecutor, baseUrl, serializer, logger, cacheManager); } public IRequestExecutor CreateRequestExecutor(IClientApiKey apiKey, AuthenticationScheme authenticationScheme, int connectionTimeout, ILogger logger) diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj index ebc84082..4e6953b2 100644 --- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj +++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj @@ -84,10 +84,10 @@ - + - - + + @@ -112,17 +112,20 @@ - + - + + + + @@ -130,14 +133,14 @@ - - - - - - - - + + + + + + + + @@ -269,9 +272,7 @@ - - - + From 6de94d16f0011e283e2edba8ecd922e15f087ce8 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Thu, 10 Sep 2015 17:21:34 -0700 Subject: [PATCH 167/238] Remove Net.Http and split RequestExecutor from Impl --- .../Stormpath.Demo/Stormpath.Demo.csproj | 21 -- Stormpath.SDK/Stormpath.Demo/packages.config | 6 - ...ormpath.SDK.JsonNetSerializer.Tests.csproj | 2 - .../packages.config | 2 - .../Stormpath.SDK.JsonNetSerializer.csproj | 7 - .../packages.config | 2 - .../Properties/AssemblyInfo.cs | 53 ++++ .../RestSharpRequestExecutor.cs | 23 ++ .../Stormpath.SDK.RestSharpClient.csproj | 76 ++++++ .../packages.config | 6 + .../stylecop.json | 8 + .../Stormpath.SDK.Tests.Integration.csproj | 14 +- .../packages.config | 3 - .../Fakes/FakeDataStore.cs | 24 +- .../Fakes/StubDataStore.cs | 4 +- .../Fakes/StubRequestExecutor.cs | 2 +- .../BasicRequestAuthenticator_tests.cs | 1 + .../SAuthc1RequestAuthenticator_tests.cs | 1 + .../Impl/Cache/DefaultCacheResolver_tests.cs | 1 - .../Impl/CanonicalUri_tests.cs | 2 +- .../Impl/CreationOptions_tests.cs | 6 +- ...s => DefaultCacheProviderBuilder_tests.cs} | 32 +-- .../Impl/DefaultClientBuilder_tests.cs | 44 +--- .../Impl/DefaultDataStore_tests.cs | 1 + .../DefaultJsonSerializerBuilder_tests.cs | 79 ++++++ .../Impl/DefaultJsonSerializerLoader_tests.cs | 3 +- .../Impl/FilterChain_tests.cs | 2 +- .../Impl/HttpHeaders_tests.cs | 2 +- .../Impl/QueryString_tests.cs | 2 +- .../Utility/ImmutableValueObject_tests.cs | 2 +- .../Stormpath.SDK.Tests.csproj | 18 +- .../Stormpath.SDK.Tests/packages.config | 3 - Stormpath.SDK/Stormpath.SDK.sln | 6 + Stormpath.SDK/Stormpath.SDK/Client/IClient.cs | 4 - .../Stormpath.SDK/Client/IClientBuilder.cs | 3 + .../Stormpath.SDK/DataStore/IDataStore.cs | 3 +- .../AuthorizationHeaderValue.cs | 6 +- .../Http/Support => Http}/CanonicalUri.cs | 5 +- .../{Impl => }/Http/HttpHeaders.cs | 5 +- .../{Impl/Http/Support => Http}/HttpMethod.cs | 4 +- .../{Impl => }/Http/IHttpMessage.cs | 4 +- .../{Impl => }/Http/IHttpRequest.cs | 6 +- .../{Impl => }/Http/IHttpResponse.cs | 4 +- .../{Impl => }/Http/IRequestExecutor.cs | 9 +- .../Http/Support => Http}/QueryString.cs | 5 +- .../{Impl => }/Http/RequestException.cs | 4 +- .../Impl/Api/DefaultClientApiKey.cs | 2 +- .../Client/DefaultCacheProviderBuilder.cs | 61 +++++ .../Impl/Client/DefaultClient.cs | 57 +++-- .../Impl/Client/DefaultClientBuilder.cs | 56 +++-- .../Client/DefaultJsonSerializerBuilder.cs | 59 +++++ .../Client/DefaultRequestExecutorBuilder.cs | 76 ++++++ ...erResolver.cs => ICacheProviderBuilder.cs} | 20 +- .../IJsonSerializerBuilder.cs} | 10 +- .../Impl/Client/IRequestExecutorBuilder.cs | 40 +++ .../Impl/DataStore/DefaultDataStore.cs | 1 + .../DataStore/DefaultResourceDataRequest.cs | 2 +- .../DataStore/DefaultResourceDataResult.cs | 2 +- .../Impl/DataStore/IInternalDataStore.cs | 2 +- .../Impl/DataStore/IResourceDataRequest.cs | 2 +- .../Impl/DataStore/IResourceDataResult.cs | 2 +- .../Stormpath.SDK/Impl/Error/DefaultError.cs | 2 +- .../BasicRequestAuthenticator.cs | 2 +- .../Authentication/IRequestAuthenticator.cs | 1 + .../RequestAuthenticationException.cs | 1 + .../SAuthc1RequestAuthenticator.cs | 2 +- .../Impl/Http/DefaultHttpClientLoader.cs | 33 +++ .../Impl/Http/DefaultHttpRequest.cs | 2 +- .../Impl/Http/DefaultHttpResponse.cs | 2 + .../Impl/Http/HttpMessageBase.cs | 2 + .../Stormpath.SDK/Impl/Http/NetHttpAdapter.cs | 100 -------- .../Impl/Http/NetHttpRequestExecutor.cs | 231 ------------------ .../Stormpath.SDK/Impl/InternalFactory.cs | 9 +- .../Impl/Resource/LinkProperty.cs | 2 +- .../DefaultJsonSerializerLoader.cs | 23 +- .../FieldConverters/FieldConverterResult.cs | 2 +- .../Stormpath.SDK/Impl/Utility/ITypeLoader.cs | 24 ++ .../Stormpath.SDK/Impl/Utility/TypeLoader.cs | 51 ++++ .../Impl/Utility/WindowsVersion.cs | 2 + .../ImmutableValueObject.cs | 5 +- .../Stormpath.SDK/Stormpath.SDK.csproj | 55 ++--- Stormpath.SDK/Stormpath.SDK/packages.config | 3 - 82 files changed, 822 insertions(+), 644 deletions(-) delete mode 100644 Stormpath.SDK/Stormpath.Demo/packages.config create mode 100644 Stormpath.SDK/Stormpath.SDK.RestSharpClient/Properties/AssemblyInfo.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpRequestExecutor.cs create mode 100644 Stormpath.SDK/Stormpath.SDK.RestSharpClient/Stormpath.SDK.RestSharpClient.csproj create mode 100644 Stormpath.SDK/Stormpath.SDK.RestSharpClient/packages.config create mode 100644 Stormpath.SDK/Stormpath.SDK.RestSharpClient/stylecop.json rename Stormpath.SDK/Stormpath.SDK.Tests/Impl/{DefaultCacheProviderResolver_tests.cs => DefaultCacheProviderBuilder_tests.cs} (62%) create mode 100644 Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultJsonSerializerBuilder_tests.cs rename Stormpath.SDK/Stormpath.SDK/{Impl/Http/Support => Http}/AuthorizationHeaderValue.cs (90%) rename Stormpath.SDK/Stormpath.SDK/{Impl/Http/Support => Http}/CanonicalUri.cs (97%) rename Stormpath.SDK/Stormpath.SDK/{Impl => }/Http/HttpHeaders.cs (95%) rename Stormpath.SDK/Stormpath.SDK/{Impl/Http/Support => Http}/HttpMethod.cs (96%) rename Stormpath.SDK/Stormpath.SDK/{Impl => }/Http/IHttpMessage.cs (92%) rename Stormpath.SDK/Stormpath.SDK/{Impl => }/Http/IHttpRequest.cs (86%) rename Stormpath.SDK/Stormpath.SDK/{Impl => }/Http/IHttpResponse.cs (90%) rename Stormpath.SDK/Stormpath.SDK/{Impl => }/Http/IRequestExecutor.cs (81%) rename Stormpath.SDK/Stormpath.SDK/{Impl/Http/Support => Http}/QueryString.cs (97%) rename Stormpath.SDK/Stormpath.SDK/{Impl => }/Http/RequestException.cs (91%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultCacheProviderBuilder.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultJsonSerializerBuilder.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultRequestExecutorBuilder.cs rename Stormpath.SDK/Stormpath.SDK/Impl/Client/{DefaultCacheProviderResolver.cs => ICacheProviderBuilder.cs} (56%) rename Stormpath.SDK/Stormpath.SDK/Impl/{Serialization/IJsonSerializerLoader.cs => Client/IJsonSerializerBuilder.cs} (72%) create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Client/IRequestExecutorBuilder.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultHttpClientLoader.cs delete mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpAdapter.cs delete mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Http/NetHttpRequestExecutor.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/ITypeLoader.cs create mode 100644 Stormpath.SDK/Stormpath.SDK/Impl/Utility/TypeLoader.cs rename Stormpath.SDK/Stormpath.SDK/{Impl/Utility => Shared}/ImmutableValueObject.cs (96%) diff --git a/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj b/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj index 453dd58a..6a3c6508 100644 --- a/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj +++ b/Stormpath.SDK/Stormpath.Demo/Stormpath.Demo.csproj @@ -42,14 +42,6 @@ - - ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll - True - - - ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll - True - @@ -64,7 +56,6 @@ - @@ -73,18 +64,6 @@ - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/packages.config b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/packages.config new file mode 100644 index 00000000..f82813ec --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/stylecop.json b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/stylecop.json new file mode 100644 index 00000000..1ce61c72 --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/stylecop.json @@ -0,0 +1,8 @@ +{ + "settings": { + "documentationRules": { + "companyName": "Stormpath, Inc.", + "copyrightText": "Copyright (c) 2015 Stormpath, Inc." + } + } +} \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj index 37f94413..a8ab9f5d 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Stormpath.SDK.Tests.Integration.csproj @@ -44,14 +44,6 @@ - - ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll - True - - - ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll - True - @@ -96,6 +88,10 @@ {4011b5ea-ceb5-4212-b8e6-d18c63ab6acf} Stormpath.SDK.JsonNetSerializer + + {21f7a025-7bb1-49b0-8ad3-cee8a3b1d542} + Stormpath.SDK.RestSharpClient + {79a65c37-9db1-413a-ac23-708404530295} Stormpath.SDK @@ -120,10 +116,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/packages.config b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/packages.config new file mode 100644 index 00000000..67010d9a --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/Properties/AssemblyInfo.cs b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/Properties/AssemblyInfo.cs index 76896ece..c1f206cd 100644 --- a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/Properties/AssemblyInfo.cs +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/Properties/AssemblyInfo.cs @@ -51,3 +51,4 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: InternalsVisibleTo("Stormpath.SDK.RestsharpClient.Tests")] diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpAdapter.cs b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpAdapter.cs index e1aa77ed..b40086f6 100644 --- a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpAdapter.cs +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpAdapter.cs @@ -75,7 +75,13 @@ public SDK.Http.IHttpResponse ToHttpResponse(RestSharp.IRestResponse response) private void CopyHeaders(SDK.Http.HttpHeaders httpHeaders, RestSharp.IRestRequest restRequest) { - throw new NotImplementedException(); + foreach (var header in httpHeaders) + { + foreach (var value in header.Value) + { + restRequest.AddHeader(header.Key, value); + } + } } private SDK.Http.HttpHeaders ToHttpHeaders(IList restSharpHeaders) @@ -92,8 +98,25 @@ private SDK.Http.HttpHeaders ToHttpHeaders(IList restSharpH private RestSharp.Method ConvertMethod(SDK.Http.HttpMethod httpMethod) { - // TODO - throw new NotImplementedException(); + if (httpMethod == SDK.Http.HttpMethod.Delete) + return RestSharp.Method.DELETE; + + if (httpMethod == SDK.Http.HttpMethod.Get) + return RestSharp.Method.GET; + + if (httpMethod == SDK.Http.HttpMethod.Head) + return RestSharp.Method.HEAD; + + if (httpMethod == SDK.Http.HttpMethod.Options) + return RestSharp.Method.OPTIONS; + + if (httpMethod == SDK.Http.HttpMethod.Post) + return RestSharp.Method.POST; + + if (httpMethod == SDK.Http.HttpMethod.Put) + return RestSharp.Method.PUT; + + throw new ArgumentException($"Unknown method type {httpMethod.DisplayName}", nameof(httpMethod)); } } } diff --git a/Stormpath.SDK/Stormpath.SDK.sln b/Stormpath.SDK/Stormpath.SDK.sln index 625d90f6..a57ba25b 100644 --- a/Stormpath.SDK/Stormpath.SDK.sln +++ b/Stormpath.SDK/Stormpath.SDK.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.JsonNetSerial EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.RestSharpClient", "Stormpath.SDK.RestSharpClient\Stormpath.SDK.RestSharpClient.csproj", "{21F7A025-7BB1-49B0-8AD3-CEE8A3B1D542}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stormpath.SDK.RestSharpClient.Tests", "Stormpath.SDK.RestSharpClient.Tests\Stormpath.SDK.RestSharpClient.Tests.csproj", "{64893BF7-F4AA-4374-B81B-4572989852EB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,10 @@ Global {21F7A025-7BB1-49B0-8AD3-CEE8A3B1D542}.Debug|Any CPU.Build.0 = Debug|Any CPU {21F7A025-7BB1-49B0-8AD3-CEE8A3B1D542}.Release|Any CPU.ActiveCfg = Release|Any CPU {21F7A025-7BB1-49B0-8AD3-CEE8A3B1D542}.Release|Any CPU.Build.0 = Release|Any CPU + {64893BF7-F4AA-4374-B81B-4572989852EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64893BF7-F4AA-4374-B81B-4572989852EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64893BF7-F4AA-4374-B81B-4572989852EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64893BF7-F4AA-4374-B81B-4572989852EB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From f18fcf59f9c0cd88543e5de238ab6fa3eeedf54d Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 14 Sep 2015 10:50:52 -0700 Subject: [PATCH 177/238] Minor formatting fix --- Stormpath.SDK/Stormpath.Demo/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stormpath.SDK/Stormpath.Demo/Program.cs b/Stormpath.SDK/Stormpath.Demo/Program.cs index 1eb02535..233641e3 100644 --- a/Stormpath.SDK/Stormpath.Demo/Program.cs +++ b/Stormpath.SDK/Stormpath.Demo/Program.cs @@ -86,7 +86,7 @@ static async Task MainAsync(CancellationToken cancellationToken) vader.SetPassword("1Findyourlackofsecuritydisturbing!"); addedUsers.Add( await myApp.CreateAccountAsync(vader, - options => options.RegistrationWorkflowEnabled = false, + options => options.RegistrationWorkflowEnabled = false, cancellationToken)); // List all accounts (this time with an asynchronous foreach) From fe8fb8db63f3f094878447e7d05385c1ebf11f69 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 14 Sep 2015 14:13:09 -0700 Subject: [PATCH 178/238] All ITs passing for #12 --- .../RestSharpAdapter_tests.cs | 26 +++++++++++--- .../RestSharpAdapter.cs | 20 +++++------ .../RestSharpClient.cs | 35 ++++++++++++------- .../Impl/DefaultHttpClientBuilder_tests.cs | 2 +- .../Impl/DefaultHttpClientLoader_tests.cs | 2 +- .../Http/IAsynchronousHttpClient.cs | 2 +- .../Impl/Client/DefaultClientBuilder.cs | 1 + .../Impl/Client/DefaultHttpClientBuilder.cs | 9 ++++- .../Impl/Client/IHttpClientBuilder.cs | 2 ++ .../Impl/DataStore/DefaultDataStore.cs | 14 +++++++- .../Impl/Http/DefaultRequestExecutor.cs | 7 ++-- 11 files changed, 85 insertions(+), 35 deletions(-) diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/RestSharpAdapter_tests.cs b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/RestSharpAdapter_tests.cs index f55b60b6..470da3f0 100644 --- a/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/RestSharpAdapter_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/RestSharpAdapter_tests.cs @@ -23,6 +23,8 @@ namespace Stormpath.SDK.Extensions.Http.RestSharpClient.Tests { public class RestSharpAdapter_tests { + private readonly string baseUrl = "http://api.foo.bar/v1"; + [Fact] public void Get_request_is_converted_properly() { @@ -33,15 +35,16 @@ public void Get_request_is_converted_properly() request.Method.Returns(SDK.Http.HttpMethod.Get); request.Body.Returns(string.Empty); request.BodyContentType.Returns(string.Empty); - request.CanonicalUri.Returns(new SDK.Http.CanonicalUri("http://api.foo.bar/baz")); + request.CanonicalUri.Returns(new SDK.Http.CanonicalUri("http://api.foo.bar/v1/baz")); request.Headers.Returns(headers); var adapter = new RestSharpAdapter(); - var convertedRequest = adapter.ToRestRequest(request); + var convertedRequest = adapter.ToRestRequest(this.baseUrl, request); convertedRequest.Method.ShouldBe(RestSharp.Method.GET); convertedRequest.RequestFormat.ShouldBe(RestSharp.DataFormat.Json); convertedRequest.Resource.ShouldBe("/baz"); + convertedRequest.Parameters.ShouldContain(p => p.Type == RestSharp.ParameterType.HttpHeader && p.Name == "Authorization" && @@ -62,22 +65,37 @@ public void Post_request_is_converted_properly() request.BodyContentType.Returns(string.Empty); request.HasBody.Returns(true); - request.CanonicalUri.Returns(new SDK.Http.CanonicalUri("http://api.foo.bar/baz")); + request.CanonicalUri.Returns(new SDK.Http.CanonicalUri("http://api.foo.bar/v1/baz")); request.Headers.Returns(headers); var adapter = new RestSharpAdapter(); - var convertedRequest = adapter.ToRestRequest(request); + var convertedRequest = adapter.ToRestRequest(this.baseUrl, request); convertedRequest.Method.ShouldBe(RestSharp.Method.POST); convertedRequest.RequestFormat.ShouldBe(RestSharp.DataFormat.Json); convertedRequest.Resource.ShouldBe("/baz"); + convertedRequest.Parameters.ShouldContain(p => p.Type == RestSharp.ParameterType.HttpHeader && p.Name == "Authorization" && (string)p.Value == "Basic foobarabc123"); + convertedRequest.Parameters.ShouldContain(p => p.Type == RestSharp.ParameterType.RequestBody && (string)p.Value == fakeBody); } + + [Fact] + public void Resource_URLs_are_constructed_properly() + { + var request = Substitute.For(); + request.Method.Returns(SDK.Http.HttpMethod.Put); + request.CanonicalUri.Returns(new SDK.Http.CanonicalUri("http://api.foo.bar/v1/baz")); + + var adapter = new RestSharpAdapter(); + var convertedRequest = adapter.ToRestRequest("http://api.foo.bar/v1", request); + + convertedRequest.Resource.ShouldBe("/baz"); + } } } diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpAdapter.cs b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpAdapter.cs index b40086f6..34cbbe31 100644 --- a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpAdapter.cs +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpAdapter.cs @@ -23,22 +23,17 @@ namespace Stormpath.SDK.Extensions.Http { internal class RestSharpAdapter { - public RestSharp.IRestRequest ToRestRequest(SDK.Http.IHttpRequest request) + public RestSharp.IRestRequest ToRestRequest(string baseUrl, SDK.Http.IHttpRequest request) { + var resourcePath = request.CanonicalUri.ToString().Replace(baseUrl, string.Empty); var method = this.ConvertMethod(request.Method); - var restRequest = new RestSharp.RestRequest(request.CanonicalUri.ToUri(), method); + + var restRequest = new RestSharp.RestRequest(resourcePath, method); restRequest.RequestFormat = RestSharp.DataFormat.Json; + this.CopyHeaders(request.Headers, restRequest); if (request.HasBody) - { - restRequest.AddParameter(new RestSharp.Parameter() - { - Type = RestSharp.ParameterType.RequestBody, - Value = request.Body - }); - } - - this.CopyHeaders(request.Headers, restRequest); + restRequest.AddParameter(request.BodyContentType, request.Body, RestSharp.ParameterType.RequestBody); return restRequest; } @@ -75,6 +70,9 @@ public SDK.Http.IHttpResponse ToHttpResponse(RestSharp.IRestResponse response) private void CopyHeaders(SDK.Http.HttpHeaders httpHeaders, RestSharp.IRestRequest restRequest) { + if (httpHeaders == null) + return; + foreach (var header in httpHeaders) { foreach (var value in header.Value) diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpClient.cs b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpClient.cs index 128b4068..27f7e445 100644 --- a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpClient.cs +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpClient.cs @@ -28,6 +28,7 @@ namespace Stormpath.SDK.Extensions.Http public sealed class RestSharpClient : SDK.Http.ISynchronousHttpClient, SDK.Http.IAsynchronousHttpClient { private readonly RestSharpAdapter adapter; + private readonly string baseUrl; private readonly int connectionTimeout; private readonly ILogger logger; @@ -35,37 +36,45 @@ public sealed class RestSharpClient : SDK.Http.ISynchronousHttpClient, SDK.Http. bool SDK.Http.IHttpClient.IsSynchronousSupported => false; // TODO - bool SDK.Http.IHttpClient.IsAsynchronousSupported => false; // TODO + bool SDK.Http.IHttpClient.IsAsynchronousSupported => true; - public RestSharpClient() - : this(0, null) - { - } - - public RestSharpClient(int connectionTimeout, ILogger logger) + public RestSharpClient(string baseUrl, int connectionTimeout, ILogger logger) { this.adapter = new RestSharpAdapter(); + this.baseUrl = baseUrl; this.connectionTimeout = connectionTimeout; this.logger = logger; } - private RestSharp.IRestClient GetClient() + private RestSharp.IRestClient CreateClientForRequest(SDK.Http.IHttpRequest request) { var client = new RestSharp.RestClient(); // Configure default settings + client.BaseUrl = new Uri(this.baseUrl, UriKind.Absolute); + client.DefaultParameters.Clear(); client.Encoding = Encoding.UTF8; client.FollowRedirects = false; client.Timeout = this.connectionTimeout; + client.UserAgent = request.Headers?.UserAgent; return client; } + private static bool IsValidBaseUrl(RestSharp.IRestClient client, SDK.Http.IHttpRequest request) + { + return request.CanonicalUri + .ToString() + .Contains(client.BaseUrl.ToString()); + } + SDK.Http.IHttpResponse SDK.Http.ISynchronousHttpClient.Execute(SDK.Http.IHttpRequest request) { - var client = this.GetClient(); - var restRequest = this.adapter.ToRestRequest(request); + var client = this.CreateClientForRequest(request); + if (!IsValidBaseUrl(client, request)) + throw new ApplicationException($"Request URI '{request.CanonicalUri.ToString()}' does not match client base URI '{client.BaseUrl}"); + var restRequest = this.adapter.ToRestRequest(this.baseUrl, request); var response = client.Execute(restRequest); return this.adapter.ToHttpResponse(response); @@ -73,9 +82,11 @@ SDK.Http.IHttpResponse SDK.Http.ISynchronousHttpClient.Execute(SDK.Http.IHttpReq async Task SDK.Http.IAsynchronousHttpClient.ExecuteAsync(SDK.Http.IHttpRequest request, CancellationToken cancellationToken) { - var client = this.GetClient(); - var restRequest = this.adapter.ToRestRequest(request); + var client = this.CreateClientForRequest(request); + if (!IsValidBaseUrl(client, request)) + throw new ApplicationException($"Request URI '{request.CanonicalUri.ToString()}' does not match client base URI '{client.BaseUrl}"); + var restRequest = this.adapter.ToRestRequest(this.baseUrl, request); var response = await client.ExecuteTaskAsync(restRequest, cancellationToken).ConfigureAwait(false); return this.adapter.ToHttpResponse(response); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientBuilder_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientBuilder_tests.cs index 07f204d7..2161168b 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientBuilder_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientBuilder_tests.cs @@ -43,7 +43,7 @@ public void Constructs_instance_from_specified_type() { IHttpClientBuilder builder = new DefaultHttpClientBuilder(this.GetFailingLoader()); - builder.UseHttpClient(new RestSharpClient()); + builder.UseHttpClient(new RestSharpClient("http://api.foo.bar", 0, Substitute.For())); var instance = builder.Build(); instance.ShouldBeAssignableTo(); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientLoader_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientLoader_tests.cs index fc1cfe19..e2a3560a 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientLoader_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientLoader_tests.cs @@ -34,7 +34,7 @@ public void Default_library_is_loaded() // This test project has a reference to Stormpath.SDK.RestSharpClient, so the file lookup will succeed IHttpClient instance = null; - var constructorArgs = new object[] { 100, Substitute.For() }; + var constructorArgs = new object[] { "http://api.foo.bar", 100, Substitute.For() }; bool loadResult = loader.TryLoad(out instance, constructorArgs); loadResult.ShouldBe(true); diff --git a/Stormpath.SDK/Stormpath.SDK/Http/IAsynchronousHttpClient.cs b/Stormpath.SDK/Stormpath.SDK/Http/IAsynchronousHttpClient.cs index bb9eef94..f765087d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Http/IAsynchronousHttpClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Http/IAsynchronousHttpClient.cs @@ -22,6 +22,6 @@ namespace Stormpath.SDK.Http { public interface IAsynchronousHttpClient : IHttpClient { - Task ExecuteAsync(IHttpRequest request, CancellationToken cancellationToken); + Task ExecuteAsync(IHttpRequest request, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs index 67db8537..58b2da79 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultClientBuilder.cs @@ -127,6 +127,7 @@ IClient IClientBuilder.Build() throw new ArgumentException("API Key is not valid or has not been set. Use ClientApiKeys.Builder() to construct one."); this.httpClientBuilder + .SetBaseUrl(this.baseUrl) .SetConnectionTimeout(this.connectionTimeout); return new DefaultClient( diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultHttpClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultHttpClientBuilder.cs index d02d78ef..a6ee1ec8 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultHttpClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultHttpClientBuilder.cs @@ -28,6 +28,7 @@ internal sealed class DefaultHttpClientBuilder : IHttpClientBuilder private readonly ITypeLoader defaultLibraryLoader; private IHttpClient instance; + private string baseUrl; private int connectionTimeout; private ILogger logger; @@ -47,6 +48,12 @@ IHttpClientBuilder IHttpClientBuilder.UseHttpClient(IHttpClient client) return this; } + IHttpClientBuilder IHttpClientBuilder.SetBaseUrl(string baseUrl) + { + this.baseUrl = baseUrl; + return this; + } + IHttpClientBuilder IHttpClientBuilder.SetConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; @@ -65,7 +72,7 @@ IHttpClient IHttpClientBuilder.Build() return this.instance; IHttpClient defaultClient = null; - bool foundDefaultLibrary = this.defaultLibraryLoader.TryLoad(out defaultClient, new object[] { this.connectionTimeout, this.logger }); + bool foundDefaultLibrary = this.defaultLibraryLoader.TryLoad(out defaultClient, new object[] { this.baseUrl, this.connectionTimeout, this.logger }); if (!foundDefaultLibrary) throw new ApplicationException("Could not find a valid HTTP client. Include Stormpath.SDK.RestSharpClient.dll in the application path."); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/IHttpClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/IHttpClientBuilder.cs index b92a4341..1a3079c5 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/IHttpClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/IHttpClientBuilder.cs @@ -24,6 +24,8 @@ internal interface IHttpClientBuilder { IHttpClientBuilder UseHttpClient(IHttpClient client); + IHttpClientBuilder SetBaseUrl(string baseUrl); + IHttpClientBuilder SetConnectionTimeout(int connectionTimeout); IHttpClientBuilder SetLogger(ILogger logger); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs index 22154ff0..9facb615 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/DataStore/DefaultDataStore.cs @@ -160,7 +160,9 @@ private ResourceAction GetPostAction(IResourceDataRequest request, IHttpResponse private IHttpResponse HandleResponseOrError(IHttpResponse response) { - if (response.ErrorType > ResponseErrorType.None) + if (response.ErrorType > ResponseErrorType.None || + IsClientError(response.StatusCode) || + IsServerError(response.StatusCode)) { DefaultError error = null; error = response.HasBody @@ -173,6 +175,16 @@ private IHttpResponse HandleResponseOrError(IHttpResponse response) return response; } + private static bool IsClientError(int statusCode) + { + return statusCode >= 400 && statusCode < 500; + } + + private static bool IsServerError(int statusCode) + { + return statusCode >= 500 && statusCode < 600; + } + // DataStore methods async Task IDataStore.GetResourceAsync(string resourcePath, CancellationToken cancellationToken) { diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultRequestExecutor.cs index 6405286a..86c45bc4 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultRequestExecutor.cs @@ -147,8 +147,6 @@ private async Task CoreRequestLoopAsync( try { - attempts++; - if (attempts > this.maxAttemptsPerRequest) throw new ApplicationException("Reached maximum number of request retries."); @@ -164,7 +162,7 @@ private async Task CoreRequestLoopAsync( currentUri = response.Headers.Location; this.logger.Trace($"Redirected to {currentUri}", "DefaultRequestExecutor.CoreRequestLoopAsync"); - continue; // reexecute request + continue; // reexecute request, not counted as a retry } var statusCode = response.StatusCode; @@ -174,6 +172,7 @@ private async Task CoreRequestLoopAsync( throttling = true; this.logger.Warn($"Got HTTP 429, throttling", "DefaultRequestExecutor.CoreRequestLoopAsync"); + attempts++; continue; // retry request } @@ -181,6 +180,7 @@ private async Task CoreRequestLoopAsync( { this.logger.Warn($"Got HTTP {statusCode}", "DefaultRequestExecutor.CoreRequestLoopAsync"); + attempts++; continue; // retry request } @@ -188,6 +188,7 @@ private async Task CoreRequestLoopAsync( { this.logger.Warn($"Recoverable error during request", "DefaultRequestExecutor.CoreRequestLoopAsync"); + attempts++; continue; // retry request } From 39fb5c7703b5cc9aaa9e47119ebabf26a99a25bf Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 14 Sep 2015 14:34:52 -0700 Subject: [PATCH 179/238] Add proxy support to IHttpClient --- .../RestSharpClient.cs | 16 +++++++++++++--- .../Impl/DefaultClientBuilder_tests.cs | 2 -- .../Impl/DefaultHttpClientBuilder_tests.cs | 2 +- .../Impl/DefaultHttpClientLoader_tests.cs | 2 +- Stormpath.SDK/Stormpath.SDK/Http/IHttpClient.cs | 7 +++++++ .../Impl/Client/DefaultHttpClientBuilder.cs | 10 +++++++++- .../Impl/Client/IHttpClientBuilder.cs | 3 +++ .../Impl/Http/DefaultHttpResponse.cs | 1 - .../Impl/Http/DefaultRequestExecutor.cs | 1 - 9 files changed, 34 insertions(+), 10 deletions(-) diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpClient.cs b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpClient.cs index 27f7e445..6349bb0f 100644 --- a/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpClient.cs +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient/RestSharpClient.cs @@ -16,8 +16,7 @@ // using System; -using System.Collections.Generic; -using System.Linq; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -30,19 +29,27 @@ public sealed class RestSharpClient : SDK.Http.ISynchronousHttpClient, SDK.Http. private readonly RestSharpAdapter adapter; private readonly string baseUrl; private readonly int connectionTimeout; + private readonly IWebProxy proxy; private readonly ILogger logger; private bool alreadyDisposed = false; + string SDK.Http.IHttpClient.BaseUrl => this.baseUrl; + + int SDK.Http.IHttpClient.ConnectionTimeout => this.connectionTimeout; + + IWebProxy SDK.Http.IHttpClient.Proxy => this.proxy; + bool SDK.Http.IHttpClient.IsSynchronousSupported => false; // TODO bool SDK.Http.IHttpClient.IsAsynchronousSupported => true; - public RestSharpClient(string baseUrl, int connectionTimeout, ILogger logger) + public RestSharpClient(string baseUrl, int connectionTimeout, IWebProxy proxy, ILogger logger) { this.adapter = new RestSharpAdapter(); this.baseUrl = baseUrl; this.connectionTimeout = connectionTimeout; + this.proxy = proxy; this.logger = logger; } @@ -58,6 +65,9 @@ private RestSharp.IRestClient CreateClientForRequest(SDK.Http.IHttpRequest reque client.Timeout = this.connectionTimeout; client.UserAgent = request.Headers?.UserAgent; + if (this.proxy != null) + client.Proxy = this.proxy; + return client; } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs index b8cf82bd..bbfeae8e 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs @@ -16,9 +16,7 @@ // using System; -using NSubstitute; using Shouldly; -using Stormpath.SDK.Api; using Stormpath.SDK.Client; using Stormpath.SDK.Impl.Client; using Stormpath.SDK.Tests.Fakes; diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientBuilder_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientBuilder_tests.cs index 2161168b..9ab76cd8 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientBuilder_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientBuilder_tests.cs @@ -43,7 +43,7 @@ public void Constructs_instance_from_specified_type() { IHttpClientBuilder builder = new DefaultHttpClientBuilder(this.GetFailingLoader()); - builder.UseHttpClient(new RestSharpClient("http://api.foo.bar", 0, Substitute.For())); + builder.UseHttpClient(new RestSharpClient("http://api.foo.bar", 0, null, Substitute.For())); var instance = builder.Build(); instance.ShouldBeAssignableTo(); diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientLoader_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientLoader_tests.cs index e2a3560a..3ba6d233 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientLoader_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultHttpClientLoader_tests.cs @@ -34,7 +34,7 @@ public void Default_library_is_loaded() // This test project has a reference to Stormpath.SDK.RestSharpClient, so the file lookup will succeed IHttpClient instance = null; - var constructorArgs = new object[] { "http://api.foo.bar", 100, Substitute.For() }; + var constructorArgs = new object[] { "http://api.foo.bar", 100, null, Substitute.For() }; bool loadResult = loader.TryLoad(out instance, constructorArgs); loadResult.ShouldBe(true); diff --git a/Stormpath.SDK/Stormpath.SDK/Http/IHttpClient.cs b/Stormpath.SDK/Stormpath.SDK/Http/IHttpClient.cs index 5d45174c..1692ab36 100644 --- a/Stormpath.SDK/Stormpath.SDK/Http/IHttpClient.cs +++ b/Stormpath.SDK/Stormpath.SDK/Http/IHttpClient.cs @@ -16,11 +16,18 @@ // using System; +using System.Net; namespace Stormpath.SDK.Http { public interface IHttpClient : IDisposable { + string BaseUrl { get; } + + int ConnectionTimeout { get; } + + IWebProxy Proxy { get; } + bool IsSynchronousSupported { get; } bool IsAsynchronousSupported { get; } diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultHttpClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultHttpClientBuilder.cs index a6ee1ec8..6b1f9c84 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultHttpClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/DefaultHttpClientBuilder.cs @@ -16,6 +16,7 @@ // using System; +using System.Net; using Stormpath.SDK.Http; using Stormpath.SDK.Impl.Http; using Stormpath.SDK.Impl.Utility; @@ -30,6 +31,7 @@ internal sealed class DefaultHttpClientBuilder : IHttpClientBuilder private IHttpClient instance; private string baseUrl; private int connectionTimeout; + private IWebProxy proxy; private ILogger logger; public DefaultHttpClientBuilder() @@ -60,6 +62,12 @@ IHttpClientBuilder IHttpClientBuilder.SetConnectionTimeout(int connectionTimeout return this; } + IHttpClientBuilder IHttpClientBuilder.SetProxy(IWebProxy proxy) + { + this.proxy = proxy; + return this; + } + IHttpClientBuilder IHttpClientBuilder.SetLogger(ILogger logger) { this.logger = logger; @@ -72,7 +80,7 @@ IHttpClient IHttpClientBuilder.Build() return this.instance; IHttpClient defaultClient = null; - bool foundDefaultLibrary = this.defaultLibraryLoader.TryLoad(out defaultClient, new object[] { this.baseUrl, this.connectionTimeout, this.logger }); + bool foundDefaultLibrary = this.defaultLibraryLoader.TryLoad(out defaultClient, new object[] { this.baseUrl, this.connectionTimeout, this.proxy, this.logger }); if (!foundDefaultLibrary) throw new ApplicationException("Could not find a valid HTTP client. Include Stormpath.SDK.RestSharpClient.dll in the application path."); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Client/IHttpClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Client/IHttpClientBuilder.cs index 1a3079c5..095dfdf5 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Client/IHttpClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Client/IHttpClientBuilder.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System.Net; using Stormpath.SDK.Http; using Stormpath.SDK.Shared; @@ -28,6 +29,8 @@ internal interface IHttpClientBuilder IHttpClientBuilder SetConnectionTimeout(int connectionTimeout); + IHttpClientBuilder SetProxy(IWebProxy proxy); + IHttpClientBuilder SetLogger(ILogger logger); IHttpClient Build(); diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultHttpResponse.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultHttpResponse.cs index bb04cef1..c8011c48 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultHttpResponse.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultHttpResponse.cs @@ -15,7 +15,6 @@ // limitations under the License. // -using System.Net; using Stormpath.SDK.Http; namespace Stormpath.SDK.Impl.Http diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultRequestExecutor.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultRequestExecutor.cs index 86c45bc4..86c99f5d 100644 --- a/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultRequestExecutor.cs +++ b/Stormpath.SDK/Stormpath.SDK/Impl/Http/DefaultRequestExecutor.cs @@ -16,7 +16,6 @@ // using System; -using System.Net; using System.Threading; using System.Threading.Tasks; using Stormpath.SDK.Api; From 03f8b7aca5702502384c3cc087c680b82d480879 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 14 Sep 2015 14:59:19 -0700 Subject: [PATCH 180/238] Minor testing fixes --- .../App.config | 6 ++++++ .../Stormpath.SDK.JsonNetSerializer.Tests.csproj | 1 + .../App.config | 6 ++++++ .../Stormpath.SDK.RestSharpClient.Tests.csproj | 12 +++++++----- .../packages.config | 16 ++++++++-------- .../Helpers/TestClientBuilder.cs | 4 ++++ .../Impl/DefaultClientBuilder_tests.cs | 8 ++++++++ .../LoggerExtensions_tests.cs | 7 +++++++ .../Stormpath.SDK/Shared/LoggerExtensions.cs | 16 ++++++++-------- 9 files changed, 55 insertions(+), 21 deletions(-) create mode 100644 Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/App.config create mode 100644 Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/App.config diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/App.config b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/App.config new file mode 100644 index 00000000..92f9b50a --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/Stormpath.SDK.JsonNetSerializer.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/Stormpath.SDK.JsonNetSerializer.Tests.csproj index 0c951a3e..dd06775f 100644 --- a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/Stormpath.SDK.JsonNetSerializer.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/Stormpath.SDK.JsonNetSerializer.Tests.csproj @@ -71,6 +71,7 @@ + diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/App.config b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/App.config new file mode 100644 index 00000000..92f9b50a --- /dev/null +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/Stormpath.SDK.RestSharpClient.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/Stormpath.SDK.RestSharpClient.Tests.csproj index 390c95ca..228e3cf7 100644 --- a/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/Stormpath.SDK.RestSharpClient.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/Stormpath.SDK.RestSharpClient.Tests.csproj @@ -10,10 +10,11 @@ Properties Stormpath.SDK.Extensions.Http Stormpath.SDK.RestSharpClient.Tests - v4.5.2 + v4.5 512 + true @@ -38,7 +39,7 @@ True - ..\packages\RestSharp.105.2.3\lib\net452\RestSharp.dll + ..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll True @@ -70,9 +71,6 @@ - - - {21f7a025-7bb1-49b0-8ad3-cee8a3b1d542} @@ -86,6 +84,10 @@ + + + + diff --git a/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/packages.config b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/packages.config index 67010d9a..15603ff0 100644 --- a/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/packages.config +++ b/Stormpath.SDK/Stormpath.SDK.RestSharpClient.Tests/packages.config @@ -1,11 +1,11 @@  - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Helpers/TestClientBuilder.cs b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Helpers/TestClientBuilder.cs index 159f6a2a..997e6682 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Helpers/TestClientBuilder.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests.Integration/Helpers/TestClientBuilder.cs @@ -17,6 +17,8 @@ using System; using Stormpath.SDK.Client; +using Stormpath.SDK.Extensions.Http; +using Stormpath.SDK.Extensions.Serialization; using Xunit.Abstractions; namespace Stormpath.SDK.Tests.Integration.Helpers @@ -42,6 +44,8 @@ public IClient Build() var apiKey = IntegrationTestClients.GetApiKey(); return Clients.Builder() .SetApiKey(apiKey) + .UseHttpClient(new RestSharpClient("https://api.stormpath.com/v1", 20000, null, null)) + .UseJsonSerializer(new JsonNetSerializer()) .SetAuthenticationScheme(authentication) .Build(); } diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs index bbfeae8e..619c1631 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/DefaultClientBuilder_tests.cs @@ -18,6 +18,8 @@ using System; using Shouldly; using Stormpath.SDK.Client; +using Stormpath.SDK.Extensions.Http; +using Stormpath.SDK.Extensions.Serialization; using Stormpath.SDK.Impl.Client; using Stormpath.SDK.Tests.Fakes; using Xunit; @@ -31,6 +33,12 @@ public class DefaultClientBuilder_tests public DefaultClientBuilder_tests() { this.builder = new DefaultClientBuilder(); + + // Providing these means the tests won't try to do a dynamic assembly lookup + // which tends to screw up parallel-running tests + this.builder + .UseHttpClient(new RestSharpClient("https://api.stormpath.com/v1", 20000, null, null)) + .UseJsonSerializer(new JsonNetSerializer()); } [Fact] diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/LoggerExtensions_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/LoggerExtensions_tests.cs index b04b1f68..39d669a7 100644 --- a/Stormpath.SDK/Stormpath.SDK.Tests/LoggerExtensions_tests.cs +++ b/Stormpath.SDK/Stormpath.SDK.Tests/LoggerExtensions_tests.cs @@ -110,5 +110,12 @@ public void Fatal_with_exception() this.DidReceiveCall(logger, LogLevel.Fatal, exception); } + + [Fact] + public void When_logger_is_null_logging_is_skipped() + { + ILogger logger = null; + logger.Info("Should not throw an error"); + } } } diff --git a/Stormpath.SDK/Stormpath.SDK/Shared/LoggerExtensions.cs b/Stormpath.SDK/Stormpath.SDK/Shared/LoggerExtensions.cs index abae973c..f0824e83 100644 --- a/Stormpath.SDK/Stormpath.SDK/Shared/LoggerExtensions.cs +++ b/Stormpath.SDK/Stormpath.SDK/Shared/LoggerExtensions.cs @@ -25,48 +25,48 @@ public static class LoggerExtensions { public static void Trace(this ILogger logger, string message, string source = null) { - logger.Log(new LogEntry(LogLevel.Trace, message, source, null)); + logger?.Log(new LogEntry(LogLevel.Trace, message, source, null)); } public static void Info(this ILogger logger, string message, string source = null) { - logger.Log(new LogEntry(LogLevel.Info, message, source, null)); + logger?.Log(new LogEntry(LogLevel.Info, message, source, null)); } public static void Warn(this ILogger logger, string message, string source = null) { - logger.Log(new LogEntry(LogLevel.Warn, message, source, null)); + logger?.Log(new LogEntry(LogLevel.Warn, message, source, null)); } public static void Error(this ILogger logger, string message, string source = null) { - logger.Log(new LogEntry(LogLevel.Error, message, source, null)); + logger?.Log(new LogEntry(LogLevel.Error, message, source, null)); } public static void Fatal(this ILogger logger, string message, string source = null) { - logger.Log(new LogEntry(LogLevel.Fatal, message, source, null)); + logger?.Log(new LogEntry(LogLevel.Fatal, message, source, null)); } public static void Warn(this ILogger logger, Exception exception, string message = null, string source = null) { var logMessage = message.Nullable() ?? exception.Message; var logSource = source.Nullable() ?? exception.Source; - logger.Log(new LogEntry(LogLevel.Warn, logMessage, source, exception)); + logger?.Log(new LogEntry(LogLevel.Warn, logMessage, source, exception)); } public static void Error(this ILogger logger, Exception exception, string message = null, string source = null) { var logMessage = message.Nullable() ?? exception.Message; var logSource = source.Nullable() ?? exception.Source; - logger.Log(new LogEntry(LogLevel.Error, logMessage, source, exception)); + logger?.Log(new LogEntry(LogLevel.Error, logMessage, source, exception)); } public static void Fatal(this ILogger logger, Exception exception, string message = null, string source = null) { var logMessage = message.Nullable() ?? exception.Message; var logSource = source.Nullable() ?? exception.Source; - logger.Log(new LogEntry(LogLevel.Fatal, logMessage, source, exception)); + logger?.Log(new LogEntry(LogLevel.Fatal, logMessage, source, exception)); } } } From 3c3cdfe98df086feee110542909813b56f8a41e9 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Mon, 14 Sep 2015 15:06:12 -0700 Subject: [PATCH 181/238] Upgrade to StyleCop.Analyzers beta11 --- .../Stormpath.SDK.JsonNetSerializer.Tests.csproj | 8 ++++---- .../Stormpath.SDK.JsonNetSerializer.Tests/packages.config | 2 +- .../Stormpath.SDK.JsonNetSerializer.csproj | 8 ++++---- .../Stormpath.SDK.JsonNetSerializer/packages.config | 2 +- .../Stormpath.SDK.RestSharpClient.csproj | 4 ++-- .../Stormpath.SDK.RestSharpClient/packages.config | 2 +- .../Stormpath.SDK.Tests.Integration.csproj | 4 ++-- .../Stormpath.SDK.Tests.Integration/packages.config | 2 +- .../Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj | 4 ++-- Stormpath.SDK/Stormpath.SDK.Tests/packages.config | 2 +- Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj | 3 +-- Stormpath.SDK/Stormpath.SDK/packages.config | 2 +- 12 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/Stormpath.SDK.JsonNetSerializer.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/Stormpath.SDK.JsonNetSerializer.Tests.csproj index dd06775f..e65f4492 100644 --- a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/Stormpath.SDK.JsonNetSerializer.Tests.csproj +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/Stormpath.SDK.JsonNetSerializer.Tests.csproj @@ -75,10 +75,6 @@ - - - - @@ -92,6 +88,10 @@ Stormpath.SDK + + + + diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/packages.config b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/packages.config index 53961c6f..3e021cf0 100644 --- a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/packages.config +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer.Tests/packages.config @@ -2,7 +2,7 @@ - + diff --git a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Stormpath.SDK.JsonNetSerializer.csproj b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Stormpath.SDK.JsonNetSerializer.csproj index 0f089376..9bcc8fdc 100644 --- a/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Stormpath.SDK.JsonNetSerializer.csproj +++ b/Stormpath.SDK/Stormpath.SDK.JsonNetSerializer/Stormpath.SDK.JsonNetSerializer.csproj @@ -56,16 +56,16 @@ - - - - {79a65c37-9db1-413a-ac23-708404530295} Stormpath.SDK + + + +