├── .github ├── dependabot.yml └── workflows │ └── dotnet-core.yml ├── .gitignore ├── Autodesk.Forge.sln ├── CHANGELOG.md ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE.md ├── README.md ├── images └── logo_forge-2-line.png ├── src ├── Autodesk.Forge.Core.E2eTestHelpers │ ├── Autodesk.Forge.Core.E2eTestHelpers.csproj │ ├── HttpResponseMessageConverter.cs │ ├── RecordingScope.cs │ ├── ReplayingScope.cs │ ├── TestHandler.cs │ ├── TestOrderer.cs │ └── TestScope.cs ├── Autodesk.Forge.Core │ ├── ApiResponse.cs │ ├── Autodesk.Forge.Core.csproj │ ├── ForgeAgentConfiguration.cs │ ├── ForgeAgentHandler.cs │ ├── ForgeConfiguration.cs │ ├── ForgeHandler.cs │ ├── ForgeService.cs │ ├── HttpResponseMessageExtensions.cs │ ├── LegacySampleConfigurationProvider.cs │ ├── Marshalling.cs │ ├── ServiceCollectionExtensions.cs │ └── TokenCache.cs ├── Directory.Build.props └── Directory.Build.targets └── tests └── Autodesk.Forge.Core.Test ├── Autodesk.Forge.Core.Test.csproj ├── TestAPSConfiguration.cs ├── TestForgeAgentHandler.cs ├── TestForgeConfiguration.cs ├── TestForgeHandler.cs ├── TestForgeService.cs └── TestMarshalling.cs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - szilvaa 10 | - augustogoncalves 11 | - zhuliice 12 | labels: 13 | - dependencies 14 | - dependabot 15 | 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "monthly" 20 | reviewers: 21 | - zhuliice 22 | labels: 23 | - dependencies 24 | - dependabot -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | language: [ 'csharp' ] 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup .NET Core 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | dotnet-version: 8.0.x 27 | 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v3 30 | with: 31 | languages: ${{ matrix.language }} 32 | 33 | - name: Autobuild 34 | id: build 35 | uses: github/codeql-action/autobuild@v3 36 | 37 | - name: Perform CodeQL Analysis 38 | id: CodeQL_analysis 39 | uses: github/codeql-action/analyze@v3 40 | 41 | - name: Test 42 | id: test 43 | run: dotnet test tests/Autodesk.Forge.Core.Test/Autodesk.Forge.Core.Test.csproj 44 | 45 | - name: Publish 46 | id: publish_to_Nuget 47 | if: ${{ github.event_name =='push' && github.ref == 'refs/heads/main'}} 48 | run: | 49 | dotnet msbuild src/Autodesk.Forge.Core/Autodesk.Forge.Core.csproj /t:Push 50 | dotnet msbuild src/Autodesk.Forge.Core.E2eTestHelpers/Autodesk.Forge.Core.E2eTestHelpers.csproj /t:Push 51 | env: 52 | NugetApiKey: ${{ secrets.NUGETAPIKEYBYENGOPS }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | TestResults/ 5 | *.user 6 | -------------------------------------------------------------------------------- /Autodesk.Forge.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34928.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Autodesk.Forge.Core", "src\Autodesk.Forge.Core\Autodesk.Forge.Core.csproj", "{E59655EF-C1BF-4318-B344-B2C6B67E1A74}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Autodesk.Forge.Core.Test", "tests\Autodesk.Forge.Core.Test\Autodesk.Forge.Core.Test.csproj", "{B52C434A-3AA1-4CA7-9F88-39AA2C208A67}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Autodesk.Forge.Core.E2eTestHelpers", "src\Autodesk.Forge.Core.E2eTestHelpers\Autodesk.Forge.Core.E2eTestHelpers.csproj", "{66694EE2-D632-476D-B533-589AC9B975F3}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{BC49E389-CECC-489D-ACE8-BB97869391B9}" 13 | ProjectSection(SolutionItems) = preProject 14 | CHANGELOG.md = CHANGELOG.md 15 | Directory.Build.props = Directory.Build.props 16 | Directory.Build.targets = Directory.Build.targets 17 | README.md = README.md 18 | EndProjectSection 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3E0D0E6A-0664-491F-93E8-0290B21C3D09}" 21 | ProjectSection(SolutionItems) = preProject 22 | src\Directory.Build.props = src\Directory.Build.props 23 | src\Directory.Build.targets = src\Directory.Build.targets 24 | EndProjectSection 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6E857E79-2F5E-4B70-996D-A42A445F67B4}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {E59655EF-C1BF-4318-B344-B2C6B67E1A74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {E59655EF-C1BF-4318-B344-B2C6B67E1A74}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {E59655EF-C1BF-4318-B344-B2C6B67E1A74}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {E59655EF-C1BF-4318-B344-B2C6B67E1A74}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {B52C434A-3AA1-4CA7-9F88-39AA2C208A67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {B52C434A-3AA1-4CA7-9F88-39AA2C208A67}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {B52C434A-3AA1-4CA7-9F88-39AA2C208A67}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {B52C434A-3AA1-4CA7-9F88-39AA2C208A67}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {66694EE2-D632-476D-B533-589AC9B975F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {66694EE2-D632-476D-B533-589AC9B975F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {66694EE2-D632-476D-B533-589AC9B975F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {66694EE2-D632-476D-B533-589AC9B975F3}.Release|Any CPU.Build.0 = Release|Any CPU 46 | EndGlobalSection 47 | GlobalSection(SolutionProperties) = preSolution 48 | HideSolutionNode = FALSE 49 | EndGlobalSection 50 | GlobalSection(NestedProjects) = preSolution 51 | {E59655EF-C1BF-4318-B344-B2C6B67E1A74} = {3E0D0E6A-0664-491F-93E8-0290B21C3D09} 52 | {B52C434A-3AA1-4CA7-9F88-39AA2C208A67} = {6E857E79-2F5E-4B70-996D-A42A445F67B4} 53 | {66694EE2-D632-476D-B533-589AC9B975F3} = {3E0D0E6A-0664-491F-93E8-0290B21C3D09} 54 | EndGlobalSection 55 | GlobalSection(ExtensibilityGlobals) = postSolution 56 | SolutionGuid = {D05CFF55-5724-433F-9941-337DC508F5FE} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 4.1.0 2 | 3 | * Add support for APS alternative environment variables `APS_CLIENT_ID` and `APS_CLIENT_SECRET` 4 | * `FORGE_CLIENT_ID` and `FORGE_CLIENT_SECRET` will be deprecated in future! 5 | 6 | ### 4.0.1 7 | 8 | * Add Documentation in the `Autodesk.Forge.Core` project. 9 | 10 | ### 4.0.0.0 11 | 12 | * Migrate to .Net 8 13 | 14 | ### 3.0.0.0 15 | 16 | * Migrate to .Net 6 17 | 18 | ### 2.1.0.0 19 | 20 | * Support multiple agents (clientId/clientSecret) in configuration. 21 | 22 | ### 2.0.0.0 23 | 24 | * Migrate to .Net 5 25 | * Support to return HttpStatusCode in HttpRequestException 26 | 27 | ### 1.0.0.0 28 | 29 | * 1.0.0-beta4 30 | 31 | * Support concurrency with `SemaphoreSlim` 32 | 33 | * 1.0.0-beta3 34 | 35 | * Configurable timeout 36 | 37 | * 1.0.0-beta2 38 | 39 | * Fix NuGet package settings 40 | 41 | * 1.0.0-beta1 42 | 43 | * Support for `FORGE_CLIENT_ID` and `FORGE_CLIENT_SECRET` environment variables via `ForgeAlternativeConfigurationExtensions` -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.1.1 4 | net8.0 5 | enable 6 | 7 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autodesk.Forge.Core 2 | 3 | [![Latest Version](https://img.shields.io/nuget/v/Autodesk.Forge.Core.svg?label=SDK&color=normalgreen)](https://www.nuget.org/packages/Autodesk.Forge.Core#readme-body-tab) [![Supported Frameworks](https://img.shields.io/badge/8.0-blue.svg?label=.NET)](https://www.nuget.org/packages/Autodesk.Forge.Core#supportedframeworks-body-tab) [![Build Status](https://img.shields.io/github/actions/workflow/status/Autodesk-Forge/forge-api-dotnet-core/dotnet-core.yml?branch=main&label=BUILD)](https://github.com/Autodesk-Forge/forge-api-dotnet-core/actions/workflows/dotnet-core.yml) 4 | 5 | ## Overview 6 | 7 | ### Requirements 8 | 9 | - .NET 8 or later 10 | 11 | ### Dependencies 12 | 13 | - [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) 14 | - [Polly](https://github.com/App-vNext/Polly) 15 | 16 | ### Changelog 17 | 18 | The change log for the SDK can be found in the [changelog file](CHANGELOG.md). 19 | 20 | ### Contributions 21 | 22 | Contributions are welcome! Please open a Pull Request. 23 | 24 | ## Support 25 | 26 | Please ask questions on [StackOverflow](https://stackoverflow.com/questions/ask?tags=autodesk-forge,csharp) with tag `autodesk-designautomation` tag. If it turns out that you may have found a bug, please open an issue. 27 | 28 | ## Getting Started 29 | 30 | This package is intended to be used by other packages, such as `Autodesk.Forge.DesignAutomation`. 31 | 32 | ## Versioning 33 | 34 | Using [Semantic Version](https://semver.org/) scheme following the pattern of `x.y.z.`: 35 | 36 | - `x`: MAJOR version when you make incompatible changes, 37 | - `y`: MINOR version when you add functionality in a backwards-compatible manner, and 38 | - `z`: PATCH version when you make backwards-compatible bug fixes. 39 | 40 | 41 | ## Source-code 42 | 43 | Generated with [swagger-codegen](https://github.com/swagger-api/swagger-codegen). 44 | 45 | #### Build 46 | ``` 47 | dotnet build Autodesk.Forge.Core.sln 48 | ``` 49 | 50 | #### Test 51 | ``` 52 | dotnet test Autodesk.Forge.Core.sln 53 | ``` 54 | 55 | ## License 56 | 57 | This sample is licensed under the terms of the **Apache License 2.0**. Please see the [LICENSE](LICENSE) file for full details. 58 | -------------------------------------------------------------------------------- /images/logo_forge-2-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk-Forge/forge-api-dotnet-core/93ce418a1a9e8f2c70eb63e373f1b4d972ee0365/images/logo_forge-2-line.png -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core.E2eTestHelpers/Autodesk.Forge.Core.E2eTestHelpers.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Shared code for Forge client sdks e2e tests 5 | false 6 | NU5100 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core.E2eTestHelpers/HttpResponseMessageConverter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using Newtonsoft.Json; 19 | using Newtonsoft.Json.Linq; 20 | using System.Net; 21 | using System.Net.Http.Headers; 22 | 23 | namespace Autodesk.Forge.Core.E2eTestHelpers 24 | { 25 | internal class HttpResponeMessageConverter : JsonConverter 26 | { 27 | static readonly HashSet UselessHeaders = new HashSet() 28 | { 29 | "Cache-Control", "Content-Security-Policy", "Date", "Pragma", "Set-Cookie", 30 | "X-Frame-Options", "Connection", "Expires", "Via", "x-amz-apigw-id", 31 | "X-Amz-Cf-Id", "x-amzn-RequestId", "X-Amzn-Trace-Id", "X-Cache", 32 | "x-amz-id-2", "x-amz-request-id", "ETag", 33 | "Content-Length" //this is calculated so no point checking 34 | }; 35 | 36 | public override bool CanConvert(Type objectType) 37 | { 38 | return typeof(HttpResponseMessage).IsAssignableFrom(objectType); 39 | } 40 | private static HttpContent DeserializeContent(JObject jsonContent) 41 | { 42 | if (jsonContent == null) 43 | { 44 | return null; 45 | } 46 | var content = new StringContent(jsonContent["Body"].ToString()); 47 | DeserializeHeaders(content.Headers, jsonContent); 48 | return content; 49 | } 50 | 51 | private static void DeserializeHeaders(HttpHeaders headers, JObject container) 52 | { 53 | headers.Clear(); 54 | var headersToken = (JObject)container["Headers"]; 55 | if (headersToken != null) 56 | { 57 | foreach (var header in headersToken.Properties()) 58 | { 59 | headers.TryAddWithoutValidation(header.Name, header.Value.Value()); 60 | } 61 | } 62 | } 63 | private static HttpRequestMessage DeserializeRequest(JObject json) 64 | { 65 | var msg = new HttpRequestMessage(); 66 | msg.Method = new HttpMethod(json["Method"].Value()); 67 | msg.RequestUri = new Uri(json["RequestUri"].Value()); 68 | DeserializeHeaders(msg.Headers, json); 69 | msg.Content = DeserializeContent((JObject)json["Content"]); 70 | return msg; 71 | } 72 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 73 | { 74 | var json = JObject.ReadFrom(reader); 75 | var msg = new HttpResponseMessage(); 76 | msg.StatusCode = (HttpStatusCode)json["StatusCode"].Value(); 77 | msg.RequestMessage = DeserializeRequest((JObject)json["Request"]); 78 | msg.Content = DeserializeContent((JObject)json["Content"]); 79 | DeserializeHeaders(msg.Headers, (JObject)json); 80 | return msg; 81 | } 82 | 83 | private static void SerializeHeaders(JObject container, HttpHeaders headers) 84 | { 85 | var jsonHeaders = new JObject(); 86 | foreach (var h in headers) 87 | { 88 | if (!UselessHeaders.Contains(h.Key)) 89 | { 90 | if (h.Key == "Authorization") 91 | { 92 | jsonHeaders.Add(h.Key, "***"); 93 | } 94 | else if (h.Key == "Content-Type") 95 | { 96 | var contentType = string.Join(";", h.Value); 97 | var index = contentType.IndexOf(';'); 98 | if (index >= 0) 99 | { 100 | contentType = contentType.Substring(0, contentType.IndexOf(';')); 101 | } 102 | jsonHeaders.Add(h.Key, string.Join(";", contentType)); 103 | } 104 | else 105 | { 106 | jsonHeaders.Add(h.Key, string.Join(";", h.Value)); 107 | } 108 | } 109 | } 110 | if (jsonHeaders.Count > 0) 111 | { 112 | container.Add("Headers", jsonHeaders); 113 | } 114 | } 115 | private static void SerializeContent(JObject container, HttpContent content) 116 | { 117 | var jsonContent = new JObject(); 118 | if (content != null) 119 | { 120 | var mediaType = content.Headers.ContentType?.MediaType; 121 | if (mediaType == "application/json") 122 | { 123 | var str = content.ReadAsStringAsync().Result; 124 | var body = JToken.Parse(str); 125 | if (body.Type == JTokenType.String) 126 | { 127 | jsonContent.Add("Body", str); 128 | } 129 | else 130 | { 131 | jsonContent.Add("Body", body); 132 | } 133 | } 134 | else if (mediaType == "application/x-www-form-urlencoded") 135 | { 136 | var str = content.ReadAsStringAsync().Result; 137 | jsonContent.Add("Body", str); 138 | } 139 | else if (mediaType == null) 140 | { 141 | 142 | } 143 | else if (mediaType == "multipart/form-data") 144 | { 145 | jsonContent.Add("Body", "Data not recorded"); 146 | } 147 | else 148 | { 149 | throw new JsonSerializationException("Unknown media type."); 150 | } 151 | if (jsonContent.Count > 0) 152 | { 153 | SerializeHeaders(jsonContent, content.Headers); 154 | } 155 | } 156 | if (jsonContent.Count > 0) 157 | { 158 | container.Add("Content", jsonContent); 159 | } 160 | } 161 | public static JObject SerializeRequest(HttpRequestMessage msg) 162 | { 163 | var json = new JObject(); 164 | json.Add("Method", msg.Method.Method); 165 | json.Add("RequestUri", msg.RequestUri.ToString()); 166 | SerializeHeaders(json, msg.Headers); 167 | SerializeContent(json, msg.Content); 168 | return json; 169 | } 170 | 171 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 172 | { 173 | var json = new JObject(); 174 | var msg = (HttpResponseMessage)value; 175 | json.Add("StatusCode", (int)msg.StatusCode); 176 | SerializeHeaders(json, msg.Headers); 177 | SerializeContent(json, msg.Content); 178 | json.Add("Request", SerializeRequest(msg.RequestMessage)); 179 | serializer.Serialize(writer, json); 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core.E2eTestHelpers/RecordingScope.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using Newtonsoft.Json; 19 | using Newtonsoft.Json.Linq; 20 | 21 | namespace Autodesk.Forge.Core.E2eTestHelpers 22 | { 23 | internal class RecordingScope : TestScope 24 | { 25 | private List records = new List(); 26 | private JsonSerializer serializer; 27 | 28 | public RecordingScope(string path) 29 | : base(path) 30 | { 31 | this.serializer = new JsonSerializer(); 32 | this.serializer.Converters.Add(new HttpResponeMessageConverter()); 33 | } 34 | 35 | public async override Task SendAsync(HttpMessageInvoker inner, HttpRequestMessage request, CancellationToken cancellationToken) 36 | { 37 | var response = await inner.SendAsync(request, cancellationToken); 38 | 39 | if (!TryRecordAuthentication(response)) 40 | { 41 | var json = JObject.FromObject(response, this.serializer); 42 | this.records.Add(json); 43 | } 44 | return response; 45 | } 46 | 47 | public override void Dispose() 48 | { 49 | base.Dispose(); 50 | var json = JsonConvert.SerializeObject(this.records, Formatting.Indented); 51 | File.WriteAllText(base.path, json); 52 | } 53 | 54 | public override bool IsRecording => true; 55 | } 56 | } -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core.E2eTestHelpers/ReplayingScope.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using Newtonsoft.Json; 19 | using Newtonsoft.Json.Linq; 20 | using Xunit; 21 | 22 | namespace Autodesk.Forge.Core.E2eTestHelpers 23 | { 24 | internal class ReplayingScope : TestScope 25 | { 26 | private int responseIndex; 27 | private List records; 28 | 29 | public ReplayingScope(string path) 30 | : base(path) 31 | { 32 | } 33 | 34 | public override Task SendAsync(HttpMessageInvoker inner, HttpRequestMessage request, CancellationToken cancellationToken) 35 | { 36 | var response = TryGetAuthentication(request); 37 | if (response == null) 38 | { 39 | if (this.responseIndex == 0) 40 | { 41 | var json = File.ReadAllText(base.path); 42 | this.records = JsonConvert.DeserializeObject>(json, new HttpResponeMessageConverter()); 43 | } 44 | response = this.records[this.responseIndex++]; 45 | AssertEqual(response.RequestMessage, request); 46 | } 47 | return Task.FromResult(response); 48 | } 49 | 50 | private void AssertEqual(HttpRequestMessage recorded, HttpRequestMessage incoming) 51 | { 52 | Assert.Equal(recorded.Method, incoming.Method); 53 | Assert.Equal(recorded.RequestUri, incoming.RequestUri); 54 | var jRecorded = HttpResponeMessageConverter.SerializeRequest(recorded); 55 | var jIncoming = HttpResponeMessageConverter.SerializeRequest(incoming); 56 | Assert.True(JToken.DeepEquals(jRecorded, jIncoming)); 57 | } 58 | 59 | public override bool IsRecording => false; 60 | } 61 | } -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core.E2eTestHelpers/TestHandler.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace Autodesk.Forge.Core.E2eTestHelpers 20 | { 21 | public class TestHandler : DelegatingHandler 22 | { 23 | private string basePath; 24 | private readonly AsyncLocal testScope = new AsyncLocal(); 25 | public ITestScope StartTestScope(string name) 26 | { 27 | TestScope scope; 28 | var path = Path.Combine(this.basePath, $"{name}.json"); 29 | if (File.Exists(path)) 30 | { 31 | scope = new ReplayingScope(path); 32 | } 33 | else 34 | { 35 | scope = new RecordingScope(path); 36 | } 37 | this.testScope.Value = scope; 38 | return scope; 39 | } 40 | public TestHandler(string basePath) 41 | : base(new HttpClientHandler()) 42 | { 43 | this.basePath = basePath; 44 | if (!Directory.Exists(basePath)) 45 | { 46 | throw new ArgumentException($"Folder with recordings does not exist. Looked for it here: {basePath}"); 47 | } 48 | } 49 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 50 | { 51 | if (this.testScope.Value == null) 52 | { 53 | throw new InvalidOperationException("TestScope is null. Did you forget to call StartTestScope?"); 54 | } 55 | return this.testScope.Value.SendAsync(new HttpMessageInvoker(InnerHandler), request, cancellationToken); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core.E2eTestHelpers/TestOrderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using System.Reflection; 19 | using Xunit.Abstractions; 20 | using Xunit.Sdk; 21 | 22 | namespace Autodesk.Forge.Core.E2eTestHelpers 23 | { 24 | public class OrderAttribute : Attribute 25 | { 26 | public double Weight { get; set; } 27 | } 28 | 29 | public class TestOrderer : ITestCaseOrderer 30 | { 31 | public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase 32 | { 33 | var ordered = testCases.OrderBy(test => test.TestMethod.Method.ToRuntimeMethod().GetCustomAttribute().Weight); 34 | return ordered; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core.E2eTestHelpers/TestScope.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using Newtonsoft.Json; 19 | 20 | namespace Autodesk.Forge.Core.E2eTestHelpers 21 | { 22 | public interface IMessageProcessor 23 | { 24 | Task SendAsync(HttpMessageInvoker inner, HttpRequestMessage request, CancellationToken cancellationToken); 25 | } 26 | 27 | public interface ITestScope : IDisposable 28 | { 29 | bool IsRecording { get; } 30 | } 31 | 32 | internal abstract class TestScope : IMessageProcessor, ITestScope 33 | { 34 | private HttpResponseMessage authResponse; 35 | protected string path; 36 | public TestScope(string path) 37 | { 38 | this.path = path; 39 | var authPath = Path.Combine(Path.GetDirectoryName(this.path), "authenticate.json"); 40 | if (File.Exists(authPath)) 41 | { 42 | var json = File.ReadAllText(authPath); 43 | this.authResponse = JsonConvert.DeserializeObject(json, new HttpResponeMessageConverter()); 44 | } 45 | } 46 | 47 | private bool IsAuthentication(HttpRequestMessage request) 48 | { 49 | return request.RequestUri.ToString().Contains("authentication/v2/token"); 50 | } 51 | protected bool TryRecordAuthentication(HttpResponseMessage response) 52 | { 53 | if (IsAuthentication(response.RequestMessage)) 54 | { 55 | if (this.authResponse == null) 56 | { 57 | this.authResponse = response; 58 | var authPath = Path.Combine(Path.GetDirectoryName(this.path), "authenticate.json"); 59 | File.WriteAllText(authPath, JsonConvert.SerializeObject(this.authResponse, Formatting.Indented, new HttpResponeMessageConverter())); 60 | } 61 | return true; 62 | } 63 | return false; 64 | } 65 | protected HttpResponseMessage TryGetAuthentication(HttpRequestMessage request) 66 | { 67 | if (IsAuthentication(request)) 68 | { 69 | return this.authResponse; 70 | } 71 | return null; 72 | } 73 | 74 | public abstract Task SendAsync(HttpMessageInvoker inner, HttpRequestMessage request, CancellationToken cancellationToken); 75 | 76 | public virtual void Dispose() 77 | { 78 | authResponse?.Dispose(); 79 | } 80 | 81 | public abstract bool IsRecording { get; } 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace Autodesk.Forge.Core 20 | { 21 | /// 22 | /// API Response 23 | /// 24 | public class ApiResponse : IDisposable 25 | { 26 | /// 27 | /// Gets or sets the HTTP response message. 28 | /// 29 | /// The HTTP response message. 30 | public HttpResponseMessage HttpResponse { get; private set; } 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | /// Http response message. 36 | public ApiResponse(HttpResponseMessage response) 37 | { 38 | this.HttpResponse = response; 39 | } 40 | 41 | /// 42 | /// Disposes the API response. 43 | /// 44 | public void Dispose() 45 | { 46 | HttpResponse?.Dispose(); 47 | } 48 | } 49 | 50 | /// 51 | /// API Response 52 | /// 53 | public class ApiResponse : ApiResponse 54 | { 55 | /// 56 | /// Gets content (parsed HTTP body) 57 | /// 58 | /// The data. 59 | public T Content { get; private set; } 60 | 61 | /// 62 | /// Initializes a new instance of the class. 63 | /// 64 | /// Http response message. 65 | /// content (parsed HTTP body) 66 | public ApiResponse(HttpResponseMessage response, T content) 67 | : base(response) 68 | { 69 | this.Content = content; 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/Autodesk.Forge.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Shared code for APS client sdks 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/ForgeAgentConfiguration.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace Autodesk.Forge.Core 20 | { 21 | /// 22 | /// Represents the configuration for the Forge Agent. 23 | /// 24 | public class ForgeAgentConfiguration 25 | { 26 | /// 27 | /// Gets or sets the client ID. 28 | /// 29 | public string ClientId { get; init; } 30 | 31 | /// 32 | /// Gets or sets the client secret. 33 | /// 34 | public string ClientSecret { get; init; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/ForgeAgentHandler.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace Autodesk.Forge.Core 20 | { 21 | /// 22 | /// Represents a handler for Forge agents. 23 | /// 24 | public class ForgeAgentHandler : DelegatingHandler 25 | { 26 | /// 27 | /// The default agent name. 28 | /// 29 | public const string defaultAgentName = "default"; 30 | 31 | private string user; 32 | 33 | /// 34 | /// Initializes a new instance of the class. 35 | /// 36 | /// The user associated with the agent. 37 | public ForgeAgentHandler(string user) 38 | { 39 | this.user = user; 40 | } 41 | 42 | /// 43 | /// Sends an HTTP request asynchronously. 44 | /// 45 | /// The HTTP request message. 46 | /// The cancellation token. 47 | /// The task representing the asynchronous operation. 48 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 49 | { 50 | request.Options.TryAdd(ForgeConfiguration.AgentKey.Key, user); 51 | return base.SendAsync(request, cancellationToken); 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/ForgeConfiguration.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace Autodesk.Forge.Core 20 | { 21 | /// 22 | /// Represents the configuration settings for the Forge SDK. 23 | /// 24 | public class ForgeConfiguration 25 | { 26 | /// 27 | /// Represents the key for the Forge agent in the HTTP request options. 28 | /// 29 | public static readonly HttpRequestOptionsKey AgentKey = new HttpRequestOptionsKey("Autodesk.Forge.Agent"); 30 | /// 31 | /// Represents the key for the Forge scope in the HTTP request options. 32 | /// 33 | public static readonly HttpRequestOptionsKey ScopeKey = new HttpRequestOptionsKey("Autodesk.Forge.Scope"); 34 | /// 35 | /// Represents the key for the Forge timeout in the HTTP request options. 36 | /// 37 | public static readonly HttpRequestOptionsKey TimeoutKey = new HttpRequestOptionsKey("Autodesk.Forge.Timeout"); 38 | 39 | /// 40 | /// Initializes a new instance of the class. 41 | /// 42 | public ForgeConfiguration() 43 | { 44 | this.AuthenticationAddress = new Uri("https://developer.api.autodesk.com/authentication/v2/token"); 45 | } 46 | 47 | /// 48 | /// Gets or sets the client ID. 49 | /// 50 | public string ClientId { get; set; } 51 | 52 | /// 53 | /// Gets or sets the client secret. 54 | /// 55 | public string ClientSecret { get; set; } 56 | 57 | /// 58 | /// Gets or sets the dictionary of Forge agent configurations. 59 | /// 60 | public IDictionary Agents { get; set; } 61 | 62 | /// 63 | /// Gets or sets the authentication address. 64 | /// 65 | public Uri AuthenticationAddress { get; set; } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/ForgeHandler.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using Microsoft.Extensions.Options; 19 | using Newtonsoft.Json; 20 | using Polly; 21 | using System.Net; 22 | using System.Net.Http.Headers; 23 | 24 | namespace Autodesk.Forge.Core 25 | { 26 | /// 27 | /// Represents a handler for Forge API requests. 28 | /// 29 | public class ForgeHandler : DelegatingHandler 30 | { 31 | private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); 32 | private readonly Random rand = new Random(); 33 | private readonly IAsyncPolicy resiliencyPolicies; 34 | 35 | /// 36 | /// Gets or sets the Forge configuration options. 37 | /// 38 | protected readonly IOptions configuration; 39 | 40 | /// 41 | /// Gets or sets the token cache. 42 | /// 43 | protected ITokenCache TokenCache { get; private set; } 44 | 45 | private bool IsDefaultClient(string user) => string.IsNullOrEmpty(user) || user == ForgeAgentHandler.defaultAgentName; 46 | 47 | /// 48 | /// Initializes a new instance of the class. 49 | /// 50 | /// The Forge configuration options. 51 | public ForgeHandler(IOptions configuration) 52 | { 53 | this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); 54 | this.TokenCache = new TokenCache(); 55 | this.resiliencyPolicies = GetResiliencyPolicies(GetDefaultTimeout()); 56 | } 57 | /// 58 | /// Gets the default timeout value. 59 | /// 60 | /// The default timeout value. 61 | protected virtual TimeSpan GetDefaultTimeout() 62 | { 63 | // use timeout greater than the forge gateways (10s), we handle the GatewayTimeout response 64 | return TimeSpan.FromSeconds(15); 65 | } 66 | /// 67 | /// Gets the retry parameters for resiliency policies. 68 | /// 69 | /// A tuple containing the base delay in milliseconds and the multiplier. 70 | protected virtual (int baseDelayInMs, int multiplier) GetRetryParameters() 71 | { 72 | return (500, 1000); 73 | } 74 | /// 75 | /// Sends an HTTP request asynchronously. 76 | /// 77 | /// The HTTP request message. 78 | /// The cancellation token. 79 | /// The task representing the asynchronous operation. 80 | /// Thrown when the request URI is null. 81 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 82 | { 83 | if (request.RequestUri == null) 84 | { 85 | throw new ArgumentNullException($"{nameof(HttpRequestMessage)}.{nameof(HttpRequestMessage.RequestUri)}"); 86 | } 87 | 88 | IAsyncPolicy policies; 89 | 90 | // check if request wants custom timeout 91 | if (request.Options.TryGetValue(ForgeConfiguration.TimeoutKey, out var timeoutValue)) 92 | { 93 | policies = GetResiliencyPolicies(TimeSpan.FromSeconds(timeoutValue)); 94 | } 95 | else 96 | { 97 | policies = this.resiliencyPolicies; 98 | } 99 | 100 | 101 | if (request.Headers.Authorization == null && 102 | request.Options.TryGetValue(ForgeConfiguration.ScopeKey, out _)) 103 | { 104 | // no authorization header so we manage authorization 105 | await RefreshTokenAsync(request, false, cancellationToken); 106 | // add a retry policy so that we refresh invalid tokens 107 | policies = policies.WrapAsync(GetTokenRefreshPolicy()); 108 | } 109 | return await policies.ExecuteAsync(async (ct) => await base.SendAsync(request, ct), cancellationToken); 110 | } 111 | /// 112 | /// Gets the token refresh policy. 113 | /// A policy that attempts to retry exactly once when a 401 error is received after obtaining a new token. 114 | /// 115 | /// The token refresh policy. 116 | protected virtual IAsyncPolicy GetTokenRefreshPolicy() 117 | { 118 | return Policy 119 | .HandleResult(r => r.StatusCode == HttpStatusCode.Unauthorized) 120 | .RetryAsync( 121 | retryCount: 1, 122 | onRetryAsync: async (outcome, retryNumber, context) => await RefreshTokenAsync(outcome.Result.RequestMessage, true, CancellationToken.None) 123 | ); 124 | } 125 | /// 126 | /// Gets the resiliency policies for handling HTTP requests. 127 | /// 128 | /// The timeout value for the policies. 129 | /// The resiliency policies. 130 | protected virtual IAsyncPolicy GetResiliencyPolicies(TimeSpan timeoutValue) 131 | { 132 | // Retry when HttpRequestException is thrown (low level network error) or 133 | // the server returns an error code that we think is transient 134 | // 135 | int[] retriable = { 136 | (int)HttpStatusCode.RequestTimeout, // 408 137 | 429, //too many requests 138 | (int)HttpStatusCode.BadGateway, // 502 139 | (int)HttpStatusCode.ServiceUnavailable, // 503 140 | (int)HttpStatusCode.GatewayTimeout // 504 141 | }; 142 | var (retryBaseDelay, retryMultiplier) = GetRetryParameters(); 143 | var retry = Policy 144 | .Handle() 145 | .Or()// thrown by Polly's TimeoutPolicy if the inner call times out 146 | .OrResult(response => 147 | { 148 | return retriable.Contains((int)response.StatusCode); 149 | }) 150 | .WaitAndRetryAsync( 151 | retryCount: 5, 152 | sleepDurationProvider: (retryCount, response, context) => 153 | { 154 | // First see how long the server wants us to wait 155 | var serverWait = response.Result?.Headers.RetryAfter?.Delta; 156 | // Calculate how long we want to wait in milliseconds 157 | var clientWait = (double)rand.Next(retryBaseDelay /*500*/, (int)Math.Pow(2, retryCount) * retryMultiplier /*1000*/); 158 | var wait = clientWait; 159 | if (serverWait.HasValue) 160 | { 161 | wait = serverWait.Value.TotalMilliseconds + clientWait; 162 | } 163 | return TimeSpan.FromMilliseconds(wait); 164 | }, 165 | onRetryAsync: (response, sleepTime, retryCount, content) => Task.CompletedTask); 166 | 167 | // break circuit after 3 errors and keep it broken for 1 minute 168 | var breaker = Policy 169 | .Handle() 170 | .Or()// thrown by Polly's TimeoutPolicy if the inner call times out 171 | .OrResult(response => 172 | { 173 | //we want to break the circuit if retriable errors persist or internal errors from the server 174 | return retriable.Contains((int)response.StatusCode) || 175 | response.StatusCode == HttpStatusCode.InternalServerError; 176 | }) 177 | .CircuitBreakerAsync(3, TimeSpan.FromMinutes(1)); 178 | 179 | // timeout handler 180 | // https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory#use-case-applying-timeouts 181 | var timeout = Policy.TimeoutAsync(timeoutValue); 182 | 183 | // ordering is important here! 184 | return Policy.WrapAsync(breaker, retry, timeout); 185 | } 186 | 187 | /// 188 | /// Refreshes the token asynchronously. 189 | /// 190 | /// The HTTP request message. 191 | /// A flag indicating whether to ignore the cache and always refresh the token. 192 | /// The cancellation token. 193 | /// The task representing the asynchronous operation. 194 | protected virtual async Task RefreshTokenAsync(HttpRequestMessage request, bool ignoreCache, CancellationToken cancellationToken) 195 | { 196 | if (request.Options.TryGetValue(ForgeConfiguration.ScopeKey, out var scope)) 197 | { 198 | var user = string.Empty; 199 | request.Options.TryGetValue(ForgeConfiguration.AgentKey, out user); 200 | var cacheKey = user + scope; 201 | // it is possible that multiple threads get here at the same time, only one of them should 202 | // attempt to refresh the token. 203 | // NOTE: We could use different semaphores for different cacheKey here. It is a minor optimization. 204 | await semaphore.WaitAsync(cancellationToken); 205 | try 206 | { 207 | if (ignoreCache || !TokenCache.TryGetValue(cacheKey, out var token)) 208 | { 209 | TimeSpan expiry; 210 | (token, expiry) = await this.Get2LeggedTokenAsync(user, scope, cancellationToken); 211 | TokenCache.Add(cacheKey, token, expiry); 212 | } 213 | request.Headers.Authorization = AuthenticationHeaderValue.Parse(token); 214 | } 215 | finally 216 | { 217 | semaphore.Release(); 218 | } 219 | } 220 | } 221 | 222 | /// 223 | /// Gets a 2-legged token asynchronously. 224 | /// 225 | /// The user. 226 | /// The scope. 227 | /// The cancellation token. 228 | /// A tuple containing the token and its expiry time. 229 | protected virtual async Task<(string, TimeSpan)> Get2LeggedTokenAsync(string user, string scope, CancellationToken cancellationToken) 230 | { 231 | using (var request = new HttpRequestMessage()) 232 | { 233 | var config = this.configuration.Value; 234 | var clientId = this.IsDefaultClient(user) ? config.ClientId : config.Agents[user].ClientId; 235 | if (string.IsNullOrEmpty(clientId)) 236 | { 237 | throw new ArgumentNullException($"{nameof(ForgeConfiguration)}.{nameof(ForgeConfiguration.ClientId)}"); 238 | } 239 | var clientSecret = this.IsDefaultClient(user) ? config.ClientSecret : config.Agents[user].ClientSecret; 240 | if (string.IsNullOrEmpty(clientSecret)) 241 | { 242 | throw new ArgumentNullException($"{nameof(ForgeConfiguration)}.{nameof(ForgeConfiguration.ClientSecret)}"); 243 | } 244 | var clientIdSecret = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}")); 245 | request.Content = new FormUrlEncodedContent(new List> 246 | { 247 | new KeyValuePair("grant_type", "client_credentials"), 248 | new KeyValuePair("scope", scope) 249 | }); 250 | request.Headers.Authorization = new AuthenticationHeaderValue("Basic", clientIdSecret); 251 | request.RequestUri = config.AuthenticationAddress; 252 | request.Method = HttpMethod.Post; 253 | 254 | var response = await this.resiliencyPolicies.ExecuteAsync(async () => await base.SendAsync(request, cancellationToken)); 255 | 256 | response.EnsureSuccessStatusCode(); 257 | var responseContent = await response.Content.ReadAsStringAsync(); 258 | var resValues = JsonConvert.DeserializeObject>(responseContent); 259 | return (resValues["token_type"] + " " + resValues["access_token"], TimeSpan.FromSeconds(double.Parse(resValues["expires_in"]))); 260 | } 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/ForgeService.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using Microsoft.Extensions.Configuration; 19 | using Microsoft.Extensions.DependencyInjection; 20 | 21 | namespace Autodesk.Forge.Core 22 | { 23 | /// 24 | /// Represents a service for interacting with the Autodesk Forge platform. 25 | /// 26 | public class ForgeService 27 | { 28 | /// 29 | /// Initializes a new instance of the class with the specified . 30 | /// 31 | /// The instance to be used for making HTTP requests. 32 | public ForgeService(HttpClient client) 33 | { 34 | this.Client = client ?? throw new ArgumentNullException(nameof(client)); 35 | } 36 | 37 | /// 38 | /// Gets the instance used by the Forge service. 39 | /// 40 | public HttpClient Client { get; private set; } 41 | 42 | /// 43 | /// Creates a default instance of the class. 44 | /// 45 | /// A default instance of the class. 46 | public static ForgeService CreateDefault() 47 | { 48 | var configuration = new ConfigurationBuilder() 49 | .SetBasePath(Directory.GetCurrentDirectory()) 50 | .AddJsonFile("appsettings.json", optional: true) 51 | .AddEnvironmentVariables() 52 | .Build(); 53 | 54 | var services = new ServiceCollection(); 55 | services.AddForgeService(configuration); 56 | var serviceProvider = services.BuildServiceProvider(); 57 | 58 | return serviceProvider.GetRequiredService(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/HttpResponseMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using System.Net; 19 | 20 | namespace Autodesk.Forge.Core 21 | { 22 | /// 23 | /// Ensures that the HTTP response message is a success status code. Throws exceptions for non-success status codes. 24 | /// 25 | public static class HttpResponseMessageExtensions 26 | { 27 | /// 28 | /// Ensures that the HTTP response message is a success status code. Throws exceptions for non-success status codes. 29 | /// 30 | /// The HTTP response message. 31 | /// The original HTTP response message if it is a success status code. 32 | /// Thrown when the server returns a TooManyRequests status code. 33 | /// Thrown when the server returns a non-success status code other than TooManyRequests. 34 | public static async Task EnsureSuccessStatusCodeAsync(this HttpResponseMessage msg) 35 | { 36 | string errorMessage = string.Empty; 37 | if (!msg.IsSuccessStatusCode) 38 | { 39 | // Disposing content just like HttpResponseMessage.EnsureSuccessStatusCode 40 | if (msg.Content != null) 41 | { 42 | // read more detailed error message if available 43 | errorMessage = await msg.Content.ReadAsStringAsync(); 44 | msg.Content.Dispose(); 45 | } 46 | if (!string.IsNullOrEmpty(errorMessage)) 47 | { 48 | errorMessage = $"\nMore error details:\n{errorMessage}."; 49 | } 50 | var message = $"The server returned the non-success status code {(int)msg.StatusCode} ({msg.ReasonPhrase}).{errorMessage}"; 51 | 52 | if (msg.StatusCode == HttpStatusCode.TooManyRequests) 53 | { 54 | var retryAfterHeader = msg.Headers.RetryAfter.Delta; 55 | throw new TooManyRequestsException(message, msg.StatusCode, retryAfterHeader); 56 | } 57 | 58 | throw new HttpRequestException(message, null, msg.StatusCode); 59 | } 60 | return msg; 61 | } 62 | } 63 | 64 | /// 65 | /// Exception thrown when the server returns a TooManyRequests status code. 66 | /// 67 | public class TooManyRequestsException : HttpRequestException 68 | { 69 | /// 70 | /// Exception thrown when the server returns a TooManyRequests status code. 71 | /// 72 | /// Exception message. 73 | /// Status code. 74 | /// Retry after time. 75 | public TooManyRequestsException(string message, HttpStatusCode statusCode, TimeSpan? retryAfter) 76 | :base(message, null, statusCode) 77 | { 78 | this.RetryAfter = retryAfter; 79 | } 80 | 81 | /// 82 | /// Retry after time. 83 | /// 84 | public TimeSpan? RetryAfter { get; init; } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/LegacySampleConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using Microsoft.Extensions.Configuration; 19 | 20 | namespace Autodesk.Forge.Core 21 | { 22 | /// 23 | /// Extensions for adding Forge alternative environment variables to the configuration builder. 24 | /// 25 | public static class ForgeAlternativeConfigurationExtensions 26 | { 27 | /// 28 | /// Adds Forge alternative environment variables to the configuration builder. 29 | /// 30 | /// The configuration builder. 31 | /// The configuration builder with Forge alternative environment variables added. 32 | /// 33 | [Obsolete("Use AddAPSAlternativeEnvironmentVariables instead will be removed in future release")] 34 | public static IConfigurationBuilder AddForgeAlternativeEnvironmentVariables(this IConfigurationBuilder configurationBuilder) 35 | { 36 | configurationBuilder.Add(new ForgeAlternativeConfigurationSource()); 37 | return configurationBuilder; 38 | } 39 | 40 | /// 41 | /// Adds APS alternative environment variables to the configuration builder. 42 | /// 43 | /// 44 | /// 45 | 46 | public static IConfigurationBuilder AddAPSAlternativeEnvironmentVariables(this IConfigurationBuilder configurationBuilder) 47 | { 48 | configurationBuilder.Add(new APSAlternativeConfigurationSource()); 49 | return configurationBuilder; 50 | } 51 | 52 | } 53 | 54 | 55 | 56 | /// 57 | /// Represents a configuration source for loading Forge alternative configuration. 58 | /// 59 | public class ForgeAlternativeConfigurationSource : IConfigurationSource 60 | { 61 | /// 62 | /// Builds the Forge alternative configuration provider. 63 | /// 64 | /// The configuration builder. 65 | /// The Forge alternative configuration provider. 66 | public IConfigurationProvider Build(IConfigurationBuilder builder) 67 | { 68 | return new ForgeAlternativeConfigurationProvider(); 69 | } 70 | } 71 | 72 | /// 73 | /// Loads the Forge alternative configuration from environment variables. 74 | /// 75 | public class ForgeAlternativeConfigurationProvider : ConfigurationProvider 76 | { 77 | /// 78 | /// Loads the Forge alternative configuration from environment variables. 79 | /// 80 | public override void Load() 81 | { 82 | var id = Environment.GetEnvironmentVariable("FORGE_CLIENT_ID"); 83 | if (!string.IsNullOrEmpty(id)) 84 | { 85 | this.Data.Add("Forge:ClientId", id); 86 | } 87 | var secret = Environment.GetEnvironmentVariable("FORGE_CLIENT_SECRET"); 88 | if (!string.IsNullOrEmpty(secret)) 89 | { 90 | this.Data.Add("Forge:ClientSecret", secret); 91 | } 92 | } 93 | } 94 | 95 | 96 | 97 | /// 98 | /// Represents a configuration source for loading APS alternative configuration. 99 | /// 100 | 101 | public class APSAlternativeConfigurationSource : IConfigurationSource 102 | { 103 | /// 104 | /// Build the APS Environment Configuration Provider 105 | /// 106 | /// 107 | /// 108 | public IConfigurationProvider Build(IConfigurationBuilder builder) 109 | { 110 | return new APSAlternativeConfigurationProvider(); 111 | } 112 | } 113 | 114 | /// 115 | /// Loads the APS alternative configuration from environment variables. 116 | /// 117 | 118 | public class APSAlternativeConfigurationProvider : ConfigurationProvider 119 | { 120 | /// 121 | /// Loads the APS alternative configuration from environment variables. 122 | /// 123 | public override void Load() 124 | { 125 | var id = Environment.GetEnvironmentVariable("APS_CLIENT_ID"); 126 | if (!string.IsNullOrEmpty(id)) 127 | { 128 | this.Data.Add("APS:ClientId", id); 129 | } 130 | var secret = Environment.GetEnvironmentVariable("APS_CLIENT_SECRET"); 131 | if (!string.IsNullOrEmpty(secret)) 132 | { 133 | this.Data.Add("APS:ClientSecret", secret); 134 | } 135 | } 136 | 137 | } 138 | } 139 | 140 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/Marshalling.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using Newtonsoft.Json; 19 | using System.Text; 20 | using System.Text.RegularExpressions; 21 | using System.Web; 22 | 23 | namespace Autodesk.Forge.Core 24 | { 25 | /// 26 | /// Marshalling utilities. 27 | /// 28 | public partial class Marshalling 29 | { 30 | private static string ParameterToString(object obj) 31 | { 32 | if (obj is DateTime) 33 | { 34 | // https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip 35 | return ((DateTime)obj).ToString("o"); 36 | } 37 | else 38 | { 39 | return Convert.ToString(obj); 40 | } 41 | } 42 | 43 | /// 44 | /// Deserializes the JSON string into a proper object. 45 | /// 46 | /// The HTTP response content. 47 | /// Object representation of the JSON string. 48 | public static async Task DeserializeAsync(HttpContent content) 49 | { 50 | if (content == null) 51 | { 52 | throw new ArgumentNullException(nameof(content)); 53 | } 54 | 55 | string mediaType = content.Headers.ContentType?.MediaType; 56 | if (mediaType != "application/json") 57 | { 58 | throw new ArgumentException($"Content-Type must be application/json. '{mediaType}' was specified."); 59 | } 60 | var str = await content.ReadAsStringAsync(); 61 | return JsonConvert.DeserializeObject(str); 62 | } 63 | 64 | /// 65 | /// Serialize an input (model) into JSON string and return it as HttpContent 66 | /// 67 | /// Object. 68 | /// HttpContent 69 | public static HttpContent Serialize(object obj) 70 | { 71 | // we might support other data types (like binary) in the future 72 | return new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json"); 73 | } 74 | 75 | /// 76 | /// Builds a request URI based on the provided relative path, route parameters, and query parameters. 77 | /// 78 | /// The relative path of the request URI. 79 | /// The route parameters to be replaced in the relative path. 80 | /// The query parameters to be added to the request URI. 81 | /// The built request URI. 82 | /// Thrown when relativePath, routeParameters, or queryParameters is null. 83 | public static Uri BuildRequestUri(string relativePath, IDictionary routeParameters, IDictionary queryParameters) 84 | { 85 | if (relativePath == null) 86 | { 87 | throw new ArgumentNullException(nameof(relativePath)); 88 | } 89 | if (routeParameters == null) 90 | { 91 | throw new ArgumentNullException(nameof(routeParameters)); 92 | } 93 | 94 | if (queryParameters == null) 95 | { 96 | throw new ArgumentNullException(nameof(queryParameters)); 97 | } 98 | 99 | // We have some interesting contradiction in the Swagger 2.0 spec: on one hand in states that 'path' is combined with 'basePath' to form the URL of the resource. 100 | // On the other hand, it also states that 'path' MUST start with '/'. The leading '/' must be removed to get the desired behavior. 101 | relativePath = relativePath.TrimStart('/'); 102 | 103 | // replace path parameters, note that + only needs to be encoded in the query string not in the path. 104 | relativePath = Regex.Replace(relativePath, @"\{(?\w+)\}", m => HttpUtility.UrlEncode(ParameterToString(routeParameters[m.Groups["key"].Value])).Replace("%2b", "+")); 105 | 106 | // add query parameters 107 | var query = new StringBuilder(); 108 | foreach (var kv in queryParameters) 109 | { 110 | if (kv.Value != null) 111 | { 112 | query.Append($"{HttpUtility.UrlEncode(kv.Key)}={HttpUtility.UrlEncode(ParameterToString(kv.Value))}&"); 113 | } 114 | } 115 | 116 | if (query.Length > 0) 117 | { 118 | query.Insert(0, "?"); 119 | relativePath += query.ToString(); 120 | } 121 | return new Uri(relativePath, UriKind.Relative); 122 | } 123 | 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | using Microsoft.Extensions.Configuration; 19 | using Microsoft.Extensions.DependencyInjection; 20 | 21 | namespace Autodesk.Forge.Core 22 | { 23 | /// 24 | /// Extensions for adding ForgeService to the IServiceCollection. 25 | /// 26 | public static class ServiceCollectionExtensions 27 | { 28 | /// 29 | /// Configures ForgeConfiguration with the given Configuration. It looks for key named "Forge" and uses 30 | /// the values underneath. 31 | /// Also adds ForgeService as a typed HttpClient with ForgeHandler as its MessageHandler. 32 | /// 33 | /// The IServiceCollection to add the ForgeService to. 34 | /// The IConfiguration containing the Forge configuration. 35 | /// The IHttpClientBuilder for further configuration. 36 | public static IHttpClientBuilder AddForgeService(this IServiceCollection services, IConfiguration configuration) 37 | { 38 | services.AddOptions(); 39 | services.Configure(configuration.GetSection("Forge")); 40 | services.Configure(configuration.GetSection("APS")); 41 | services.AddTransient(); 42 | return services.AddHttpClient() 43 | .AddHttpMessageHandler(); 44 | } 45 | 46 | 47 | /// 48 | /// Adds the ForgeService to the IServiceCollection with the provided user and configuration. 49 | /// It configures the ForgeConfiguration using the "Forge" section of the provided configuration. 50 | /// It also adds the ForgeHandler as a transient service. 51 | /// Finally, it adds the ForgeService as a typed HttpClient with the ForgeHandler as its MessageHandler. 52 | /// 53 | /// The IServiceCollection to add the ForgeService to. 54 | /// The user associated with the ForgeService. 55 | /// The IConfiguration containing the Forge configuration. 56 | /// The IHttpClientBuilder for further configuration. 57 | public static IHttpClientBuilder AddForgeService(this IServiceCollection services, string user, IConfiguration configuration) 58 | { 59 | services.AddOptions(); 60 | services.Configure(configuration.GetSection("Forge")); 61 | services.Configure(configuration.GetSection("APS")); 62 | services.AddTransient(); 63 | return services.AddHttpClient(user) 64 | .AddHttpMessageHandler(() => new ForgeAgentHandler(user)) 65 | .AddHttpMessageHandler(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Autodesk.Forge.Core/TokenCache.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Forge SDK 3 | * 4 | * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace Autodesk.Forge.Core 20 | { 21 | /// 22 | /// Represents a cache for storing access tokens. 23 | /// 24 | public interface ITokenCache 25 | { 26 | /// 27 | /// Adds an access token to the cache. 28 | /// 29 | /// The key associated with the access token. 30 | /// The access token to be added. 31 | /// The time span indicating the expiration time of the access token. 32 | void Add(string key, string value, TimeSpan expiresIn); 33 | /// 34 | /// Tries to get the access token from the cache. 35 | /// 36 | /// The key associated with the access token. 37 | /// The retrieved access token, if found. 38 | /// true if the access token is found in the cache and not expired; otherwise, false. 39 | bool TryGetValue(string key, out string value); 40 | } 41 | 42 | class TokenCache : ITokenCache 43 | { 44 | struct CacheEntry 45 | { 46 | string value; 47 | DateTime expiry; 48 | public CacheEntry(string value, TimeSpan expiresIn) 49 | { 50 | this.value = value; 51 | this.expiry = DateTime.UtcNow + expiresIn; 52 | } 53 | public bool IsExpired { get { return DateTime.UtcNow > expiry; } } 54 | public string Value { get { return this.value; } } 55 | } 56 | Dictionary cache = new Dictionary(); 57 | public void Add(string key, string value, TimeSpan expiresIn) 58 | { 59 | cache.Remove(key); 60 | cache.Add(key, new CacheEntry(value, expiresIn)); 61 | } 62 | 63 | public bool TryGetValue(string key, out string value) 64 | { 65 | value = null; 66 | if (cache.TryGetValue(key, out var entry) && !entry.IsExpired) 67 | { 68 | value = entry.Value; 69 | return true; 70 | } 71 | return false; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Autodesk 6 | Autodesk Inc. 7 | Apache-2.0 8 | https://github.com/Autodesk-Forge/forge-api-dotnet-core 9 | logo_forge-2-line.png 10 | For full release notes see https://github.com/Autodesk-Forge/forge-api-dotnet-core/blob/master/CHANGELOG.md 11 | README.md 12 | true 13 | true 14 | snupkg 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /tests/Autodesk.Forge.Core.Test/Autodesk.Forge.Core.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/Autodesk.Forge.Core.Test/TestAPSConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Options; 4 | using Moq; 5 | using Moq.Protected; 6 | using Newtonsoft.Json; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using Xunit; 13 | 14 | namespace Autodesk.Forge.Core.Test 15 | { 16 | public class TestAPSConfiguration 17 | { 18 | /// 19 | /// Tests using APS__ClientId and APS__ClientSecret environment variables dotnet core style 20 | /// 21 | [Fact] 22 | public void TestAPSConfigFromEnvironmentVariables_DoubleUnderscoreFormat() 23 | { 24 | Environment.SetEnvironmentVariable("APS__ClientId", "bla"); 25 | Environment.SetEnvironmentVariable("APS__ClientSecret", "blabla"); 26 | var configuration = new ConfigurationBuilder() 27 | .AddEnvironmentVariables() 28 | .Build(); 29 | 30 | var services = new ServiceCollection(); 31 | services.AddForgeService(configuration); 32 | var serviceProvider = services.BuildServiceProvider(); 33 | 34 | var config = serviceProvider.GetRequiredService>(); 35 | Assert.Equal("bla", config.Value.ClientId); 36 | Assert.Equal("blabla", config.Value.ClientSecret); 37 | } 38 | 39 | /// 40 | /// Tests using APS_CLIENT_ID and APS_CLIENT_SECRET environment variables 41 | /// 42 | 43 | [Fact] 44 | public void TestAPSConfigFromEnvironmentVariables_UnderscoreFormat() 45 | { 46 | Environment.SetEnvironmentVariable("APS_CLIENT_ID", "bla"); 47 | Environment.SetEnvironmentVariable("APS_CLIENT_SECRET", "blabla"); 48 | var configuration = new ConfigurationBuilder() 49 | .AddAPSAlternativeEnvironmentVariables() 50 | .Build(); 51 | var services = new ServiceCollection(); 52 | services.AddForgeService(configuration); 53 | var serviceProvider = services.BuildServiceProvider(); 54 | var config = serviceProvider.GetRequiredService>(); 55 | Assert.Equal("bla", config.Value.ClientId); 56 | Assert.Equal("blabla", config.Value.ClientSecret); 57 | 58 | } 59 | /// 60 | /// Tests loading APS configuration values from JSON with ClientId and ClientSecret 61 | /// 62 | 63 | [Fact] 64 | public void TestAPSConfigFromJson() 65 | { 66 | var json = @" 67 | { 68 | ""APS"" : { 69 | ""ClientId"" : ""bla"", 70 | ""ClientSecret"" : ""blabla"" 71 | } 72 | }"; 73 | var configuration = new ConfigurationBuilder() 74 | .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) 75 | .Build(); 76 | 77 | var services = new ServiceCollection(); 78 | services.AddForgeService(configuration); 79 | var serviceProvider = services.BuildServiceProvider(); 80 | 81 | var config = serviceProvider.GetRequiredService>(); 82 | Assert.Equal("bla", config.Value.ClientId); 83 | Assert.Equal("blabla", config.Value.ClientSecret); 84 | } 85 | 86 | /// 87 | /// Tests loading APS configuration values from JSON with additional agent configurations 88 | /// 89 | 90 | [Fact] 91 | public void TestAPSConfigFromJsonWithAgents() 92 | { 93 | var json = @" 94 | { 95 | ""APS"" : { 96 | ""ClientId"" : ""bla"", 97 | ""ClientSecret"" : ""blabla"", 98 | ""Agents"" : { 99 | ""user1"" : { 100 | ""ClientId"" : ""user1-bla"", 101 | ""ClientSecret"" : ""user1-blabla"" 102 | } 103 | } 104 | } 105 | }"; 106 | var configuration = new ConfigurationBuilder() 107 | .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) 108 | .Build(); 109 | 110 | var services = new ServiceCollection(); 111 | services.AddForgeService(configuration); 112 | var serviceProvider = services.BuildServiceProvider(); 113 | 114 | var config = serviceProvider.GetRequiredService>(); 115 | Assert.Equal("bla", config.Value.ClientId); 116 | Assert.Equal("blabla", config.Value.ClientSecret); 117 | Assert.Equal("user1-bla", config.Value.Agents["user1"].ClientId); 118 | Assert.Equal("user1-blabla", config.Value.Agents["user1"].ClientSecret); 119 | } 120 | 121 | /// 122 | /// Tests APS configuration for user agent "user1" and checks proper handling of authentication and request headers. 123 | /// 124 | [Fact] 125 | public async Task TestAPSUserAgent() 126 | { 127 | var json = @" 128 | { 129 | ""APS"" : { 130 | ""ClientId"" : ""bla"", 131 | ""ClientSecret"" : ""blabla"", 132 | ""Agents"" : { 133 | ""user1"" : { 134 | ""ClientId"" : ""user1-bla"", 135 | ""ClientSecret"" : ""user1-blabla"" 136 | } 137 | } 138 | } 139 | }"; 140 | var configuration = new ConfigurationBuilder() 141 | .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) 142 | .Build(); 143 | 144 | var sink = new Mock(MockBehavior.Strict); 145 | var services = new ServiceCollection(); 146 | services.AddForgeService("user1", configuration).ConfigurePrimaryHttpMessageHandler(() => sink.Object); 147 | var serviceProvider = services.BuildServiceProvider(); 148 | var config = serviceProvider.GetRequiredService>().Value; 149 | var req = new HttpRequestMessage(); 150 | req.RequestUri = new Uri("http://example.com"); 151 | req.Options.Set(ForgeConfiguration.ScopeKey, "somescope"); 152 | 153 | string user = null; 154 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny())) 155 | .ReturnsAsync(new HttpResponseMessage() 156 | { 157 | Content = new StringContent(JsonConvert.SerializeObject(new Dictionary { { "token_type", "Bearer" }, { "access_token", "blablabla" }, { "expires_in", "3" } })), 158 | StatusCode = System.Net.HttpStatusCode.OK 159 | }); 160 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri), It.IsAny())) 161 | .Callback((r, ct) => 162 | { 163 | r.Options.TryGetValue(ForgeConfiguration.AgentKey, out user); 164 | }) 165 | .ReturnsAsync(new HttpResponseMessage() 166 | { 167 | StatusCode = System.Net.HttpStatusCode.OK 168 | }); 169 | 170 | 171 | var clientFactory = serviceProvider.GetRequiredService(); 172 | var client = clientFactory.CreateClient("user1"); 173 | var resp = await client.SendAsync(req, CancellationToken.None); 174 | 175 | sink.Protected().As().Verify(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny()), Times.Once()); 176 | sink.Protected().As().Verify(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri), It.IsAny()), Times.Once()); 177 | Assert.Equal("user1", user); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/Autodesk.Forge.Core.Test/TestForgeAgentHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Options; 4 | using Moq; 5 | using Moq.Protected; 6 | using Newtonsoft.Json; 7 | using System.Text; 8 | using Xunit; 9 | 10 | namespace Autodesk.Forge.Core.Test 11 | { 12 | public class TestForgeAgentHandler 13 | { 14 | [Fact] 15 | public async Task TestUser() 16 | { 17 | var json = @" 18 | { 19 | ""Forge"" : { 20 | ""ClientId"" : ""bla"", 21 | ""ClientSecret"" : ""blabla"", 22 | ""Agents"" : { 23 | ""user1"" : { 24 | ""ClientId"" : ""user1-bla"", 25 | ""ClientSecret"" : ""user1-blabla"" 26 | } 27 | } 28 | } 29 | }"; 30 | var configuration = new ConfigurationBuilder() 31 | .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) 32 | .Build(); 33 | 34 | var sink = new Mock(MockBehavior.Strict); 35 | var services = new ServiceCollection(); 36 | services.AddForgeService("user1", configuration).ConfigurePrimaryHttpMessageHandler(() => sink.Object); 37 | var serviceProvider = services.BuildServiceProvider(); 38 | var config = serviceProvider.GetRequiredService>().Value; 39 | var req = new HttpRequestMessage(); 40 | req.RequestUri = new Uri("http://example.com"); 41 | req.Options.Set(ForgeConfiguration.ScopeKey, "somescope"); 42 | 43 | string user = null; 44 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny())) 45 | .ReturnsAsync(new HttpResponseMessage() 46 | { 47 | Content = new StringContent(JsonConvert.SerializeObject(new Dictionary { { "token_type", "Bearer" }, { "access_token", "blablabla" }, { "expires_in", "3" } })), 48 | StatusCode = System.Net.HttpStatusCode.OK 49 | }); 50 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri), It.IsAny())) 51 | .Callback((r, ct) => 52 | { 53 | r.Options.TryGetValue(ForgeConfiguration.AgentKey, out user); 54 | }) 55 | .ReturnsAsync(new HttpResponseMessage() 56 | { 57 | StatusCode = System.Net.HttpStatusCode.OK 58 | }); 59 | 60 | 61 | var clientFactory = serviceProvider.GetRequiredService(); 62 | var client = clientFactory.CreateClient("user1"); 63 | var resp = await client.SendAsync(req, CancellationToken.None); 64 | 65 | sink.Protected().As().Verify(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny()), Times.Once()); 66 | sink.Protected().As().Verify(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri), It.IsAny()), Times.Once()); 67 | Assert.Equal("user1", user); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Autodesk.Forge.Core.Test/TestForgeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Options; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace Autodesk.Forge.Core.Test 8 | { 9 | public class TestForgeConfiguration 10 | { 11 | [Fact] 12 | public void TestDefault() 13 | { 14 | var config = new ForgeConfiguration(); 15 | Assert.NotNull(config.AuthenticationAddress); 16 | } 17 | 18 | [Fact] 19 | public void TestValuesFromEnvironment() 20 | { 21 | Environment.SetEnvironmentVariable("Forge__ClientId", "bla"); 22 | Environment.SetEnvironmentVariable("Forge__ClientSecret", "blabla"); 23 | var configuration = new ConfigurationBuilder() 24 | .AddEnvironmentVariables() 25 | .Build(); 26 | 27 | var services = new ServiceCollection(); 28 | services.AddForgeService(configuration); 29 | var serviceProvider = services.BuildServiceProvider(); 30 | 31 | var config = serviceProvider.GetRequiredService>(); 32 | Assert.Equal("bla", config.Value.ClientId); 33 | Assert.Equal("blabla", config.Value.ClientSecret); 34 | } 35 | 36 | [Fact] 37 | public void TestValuesFromLegacyEnvironment() 38 | { 39 | Environment.SetEnvironmentVariable("FORGE_CLIENT_ID", "bla"); 40 | Environment.SetEnvironmentVariable("FORGE_CLIENT_SECRET", "blabla"); 41 | var configuration = new ConfigurationBuilder() 42 | .AddForgeAlternativeEnvironmentVariables() 43 | .Build(); 44 | 45 | var services = new ServiceCollection(); 46 | services.AddForgeService(configuration); 47 | var serviceProvider = services.BuildServiceProvider(); 48 | 49 | var config = serviceProvider.GetRequiredService>(); 50 | Assert.Equal("bla", config.Value.ClientId); 51 | Assert.Equal("blabla", config.Value.ClientSecret); 52 | } 53 | 54 | [Fact] 55 | public void TestValuesFromAPSEnvironment() 56 | { 57 | Environment.SetEnvironmentVariable("APS_CLIENT_ID", "bla"); 58 | Environment.SetEnvironmentVariable("APS_CLIENT_SECRET", "blabla"); 59 | var configuration = new ConfigurationBuilder() 60 | .AddAPSAlternativeEnvironmentVariables() 61 | .Build(); 62 | var services = new ServiceCollection(); 63 | services.AddForgeService(configuration); 64 | var serviceProvider = services.BuildServiceProvider(); 65 | var config = serviceProvider.GetRequiredService>(); 66 | Assert.Equal("bla", config.Value.ClientId); 67 | Assert.Equal("blabla",config.Value.ClientSecret); 68 | 69 | } 70 | 71 | [Fact] 72 | public void TestValuesFromJson() 73 | { 74 | var json = @" 75 | { 76 | ""Forge"" : { 77 | ""ClientId"" : ""bla"", 78 | ""ClientSecret"" : ""blabla"" 79 | } 80 | }"; 81 | var configuration = new ConfigurationBuilder() 82 | .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) 83 | .Build(); 84 | 85 | var services = new ServiceCollection(); 86 | services.AddForgeService(configuration); 87 | var serviceProvider = services.BuildServiceProvider(); 88 | 89 | var config = serviceProvider.GetRequiredService>(); 90 | Assert.Equal("bla", config.Value.ClientId); 91 | Assert.Equal("blabla", config.Value.ClientSecret); 92 | } 93 | 94 | [Fact] 95 | public void TestValuesFromJsonMoreAgents() 96 | { 97 | var json = @" 98 | { 99 | ""Forge"" : { 100 | ""ClientId"" : ""bla"", 101 | ""ClientSecret"" : ""blabla"", 102 | ""Agents"" : { 103 | ""user1"" : { 104 | ""ClientId"" : ""user1-bla"", 105 | ""ClientSecret"" : ""user1-blabla"" 106 | } 107 | } 108 | } 109 | }"; 110 | var configuration = new ConfigurationBuilder() 111 | .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) 112 | .Build(); 113 | 114 | var services = new ServiceCollection(); 115 | services.AddForgeService(configuration); 116 | var serviceProvider = services.BuildServiceProvider(); 117 | 118 | var config = serviceProvider.GetRequiredService>(); 119 | Assert.Equal("bla", config.Value.ClientId); 120 | Assert.Equal("blabla", config.Value.ClientSecret); 121 | Assert.Equal("user1-bla", config.Value.Agents["user1"].ClientId); 122 | Assert.Equal("user1-blabla", config.Value.Agents["user1"].ClientSecret); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/Autodesk.Forge.Core.Test/TestForgeHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using Moq; 3 | using Moq.Protected; 4 | using Newtonsoft.Json; 5 | using System.Linq.Expressions; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using Xunit; 9 | 10 | namespace Autodesk.Forge.Core.Test 11 | { 12 | // Make TokenCache public for testing purposes 13 | class TweakableForgeHandler : ForgeHandler 14 | { 15 | public TweakableForgeHandler(IOptions configuration) 16 | : base(configuration) 17 | { 18 | } 19 | public new ITokenCache TokenCache { get { return base.TokenCache; } } 20 | public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMilliseconds(200); 21 | protected override TimeSpan GetDefaultTimeout() 22 | { 23 | return DefaultTimeout; 24 | } 25 | protected override (int baseDelayInMs, int multiplier) GetRetryParameters() 26 | { 27 | return (5, 10); 28 | } 29 | } 30 | 31 | public class TestForgeHandler1 32 | { 33 | [Fact] 34 | public void TestNullConfigurationThrows() 35 | { 36 | Assert.Throws(() => new ForgeHandler(null)); 37 | } 38 | 39 | [Fact] 40 | public async Task TestNoRequestUriThrows() 41 | { 42 | var fh = new HttpMessageInvoker(new ForgeHandler(Options.Create(new ForgeConfiguration()))); 43 | await Assert.ThrowsAsync($"{nameof(HttpRequestMessage)}.{nameof(HttpRequestMessage.RequestUri)}", () => fh.SendAsync(new HttpRequestMessage(), CancellationToken.None)); 44 | } 45 | 46 | [Fact] 47 | public async Task TestNoClientIdThrows() 48 | { 49 | var fh = new HttpMessageInvoker(new ForgeHandler(Options.Create(new ForgeConfiguration()))); 50 | var req = new HttpRequestMessage(); 51 | req.RequestUri = new Uri("http://example.com"); 52 | req.Options.Set(ForgeConfiguration.ScopeKey, "somescope"); 53 | await Assert.ThrowsAsync($"{nameof(ForgeConfiguration)}.{nameof(ForgeConfiguration.ClientId)}", () => fh.SendAsync(req, CancellationToken.None)); 54 | } 55 | 56 | [Fact] 57 | public async Task TestNoClientSecretThrows() 58 | { 59 | var fh = new HttpMessageInvoker(new ForgeHandler(Options.Create(new ForgeConfiguration() { ClientId = "ClientId" }))); 60 | var req = new HttpRequestMessage(); 61 | req.RequestUri = new Uri("http://example.com"); 62 | req.Options.Set(ForgeConfiguration.ScopeKey, "somescope"); 63 | await Assert.ThrowsAsync($"{nameof(ForgeConfiguration)}.{nameof(ForgeConfiguration.ClientSecret)}", () => fh.SendAsync(req, CancellationToken.None)); 64 | } 65 | 66 | [Fact] 67 | public async Task TestFirstCallAuthenticates() 68 | { 69 | var sink = new Mock(MockBehavior.Strict); 70 | sink.Protected().As().SetupSequence(o => o.SendAsync(It.IsAny(), It.IsAny())) 71 | .ReturnsAsync(new HttpResponseMessage() 72 | { 73 | Content = new StringContent(JsonConvert.SerializeObject(new Dictionary { { "token_type", "Bearer" }, { "access_token", "blablabla" }, { "expires_in", "3" } })), 74 | StatusCode = System.Net.HttpStatusCode.OK 75 | }) 76 | .ReturnsAsync(new HttpResponseMessage() 77 | { 78 | StatusCode = System.Net.HttpStatusCode.OK 79 | }); 80 | var config = new ForgeConfiguration() 81 | { 82 | ClientId = "ClientId", 83 | ClientSecret = "ClientSecret" 84 | }; 85 | var fh = new HttpMessageInvoker(new ForgeHandler(Options.Create(config)) 86 | { 87 | InnerHandler = sink.Object 88 | }); 89 | 90 | var req = new HttpRequestMessage(); 91 | req.RequestUri = new Uri("http://example.com"); 92 | req.Options.Set(ForgeConfiguration.ScopeKey, "somescope"); 93 | await fh.SendAsync(req, CancellationToken.None); 94 | 95 | sink.Protected().As().Verify(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny()), Times.Once()); 96 | sink.Protected().As().Verify(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri), It.IsAny()), Times.Once()); 97 | } 98 | 99 | [Fact] 100 | public async Task TestFirstCallAuthenticatesNonDefaultUser() 101 | { 102 | var req = new HttpRequestMessage(); 103 | var config = new ForgeConfiguration() 104 | { 105 | ClientId = "ClientId", 106 | ClientSecret = "ClientSecret", 107 | Agents = new Dictionary() 108 | { 109 | { 110 | "user1", new ForgeAgentConfiguration() 111 | { 112 | ClientId = "user1-bla", 113 | ClientSecret = "user1-blabla" 114 | } 115 | } 116 | } 117 | }; 118 | string actualClientId = null; 119 | string actualClientSecret = null; 120 | var sink = new Mock(MockBehavior.Strict); 121 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny())) 122 | .Callback((r, ct) => 123 | { 124 | var clientIdSecret = Encoding.UTF8.GetString(Convert.FromBase64String(r.Headers.Authorization.Parameter)).Split(':'); 125 | actualClientId = clientIdSecret[0]; 126 | actualClientSecret = clientIdSecret[1]; 127 | }) 128 | .ReturnsAsync(new HttpResponseMessage() 129 | { 130 | Content = new StringContent(JsonConvert.SerializeObject(new Dictionary { { "token_type", "Bearer" }, { "access_token", "blablabla" }, { "expires_in", "3" } })), 131 | StatusCode = System.Net.HttpStatusCode.OK 132 | }); 133 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri), It.IsAny())) 134 | .ReturnsAsync(new HttpResponseMessage() 135 | { 136 | StatusCode = System.Net.HttpStatusCode.OK 137 | }); 138 | 139 | var fh = new HttpMessageInvoker(new ForgeHandler(Options.Create(config)) 140 | { 141 | InnerHandler = sink.Object 142 | }); 143 | 144 | req.RequestUri = new Uri("http://example.com"); 145 | req.Options.Set(ForgeConfiguration.ScopeKey, "somescope"); 146 | req.Options.Set(ForgeConfiguration.AgentKey, "user1"); 147 | await fh.SendAsync(req, CancellationToken.None); 148 | 149 | Assert.Equal(config.Agents["user1"].ClientId, actualClientId); 150 | Assert.Equal(config.Agents["user1"].ClientSecret, actualClientSecret); 151 | 152 | sink.Protected().As().Verify(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri), It.IsAny()), Times.Once()); 153 | } 154 | 155 | [Fact] 156 | public async Task TestRetryOnceOnAuthenticationFailure() 157 | { 158 | var newToken = "newToken"; 159 | var cachedToken = "cachedToken"; 160 | var req = new HttpRequestMessage(); 161 | req.RequestUri = new Uri("http://example.com"); 162 | var config = new ForgeConfiguration() 163 | { 164 | ClientId = "ClientId", 165 | ClientSecret = "ClientSecret" 166 | }; 167 | var sink = new Mock(MockBehavior.Strict); 168 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri && r.Headers.Authorization.Parameter == cachedToken), It.IsAny())) 169 | .ReturnsAsync(new HttpResponseMessage() 170 | { 171 | StatusCode = System.Net.HttpStatusCode.Unauthorized, 172 | RequestMessage = req 173 | }); 174 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny())) 175 | .ReturnsAsync(new HttpResponseMessage() 176 | { 177 | Content = new StringContent(JsonConvert.SerializeObject(new Dictionary { { "token_type", "Bearer" }, { "access_token", newToken }, { "expires_in", "3" } })), 178 | StatusCode = System.Net.HttpStatusCode.OK 179 | }); 180 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri && r.Headers.Authorization.Parameter == newToken), It.IsAny())) 181 | .ReturnsAsync(new HttpResponseMessage() 182 | { 183 | StatusCode = System.Net.HttpStatusCode.OK 184 | }); 185 | 186 | var fh = new TweakableForgeHandler(Options.Create(config)) 187 | { 188 | InnerHandler = sink.Object 189 | }; 190 | 191 | var scope = "somescope"; 192 | 193 | //we have token but it bad for some reason (maybe revoked) 194 | fh.TokenCache.Add(scope, $"Bearer {cachedToken}", TimeSpan.FromSeconds(300)); 195 | 196 | var invoker = new HttpMessageInvoker(fh); 197 | 198 | req.Options.Set(ForgeConfiguration.ScopeKey, scope); 199 | await invoker.SendAsync(req, CancellationToken.None); 200 | 201 | sink.VerifyAll(); 202 | } 203 | 204 | [Fact] 205 | public async Task TestRefreshExpiredToken() 206 | { 207 | var newToken = "newToken"; 208 | var cachedToken = "cachedToken"; 209 | var req = new HttpRequestMessage(); 210 | req.RequestUri = new Uri("http://example.com"); 211 | var config = new ForgeConfiguration() 212 | { 213 | ClientId = "ClientId", 214 | ClientSecret = "ClientSecret" 215 | }; 216 | var sink = new Mock(MockBehavior.Strict); 217 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny())) 218 | .ReturnsAsync(new HttpResponseMessage() 219 | { 220 | Content = new StringContent(JsonConvert.SerializeObject(new Dictionary { { "token_type", "Bearer" }, { "access_token", newToken }, { "expires_in", "3" } })), 221 | StatusCode = System.Net.HttpStatusCode.OK 222 | }); 223 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri && r.Headers.Authorization.Parameter == newToken), It.IsAny())) 224 | .ReturnsAsync(new HttpResponseMessage() 225 | { 226 | StatusCode = System.Net.HttpStatusCode.OK 227 | }); 228 | 229 | var fh = new TweakableForgeHandler(Options.Create(config)) 230 | { 231 | InnerHandler = sink.Object 232 | }; 233 | 234 | var scope = "somescope"; 235 | 236 | //we have token but it is expired already 237 | fh.TokenCache.Add(scope, $"Bearer {cachedToken}", TimeSpan.FromSeconds(0)); 238 | 239 | var invoker = new HttpMessageInvoker(fh); 240 | 241 | req.Options.Set(ForgeConfiguration.ScopeKey, scope); 242 | await invoker.SendAsync(req, CancellationToken.None); 243 | 244 | sink.VerifyAll(); 245 | } 246 | 247 | [Fact] 248 | public async Task TestRefreshExpiredTokenByOneThreadOnly() 249 | { 250 | var newToken = "newToken"; 251 | var cachedToken = "cachedToken"; 252 | var requestUri = new Uri("http://example.com"); 253 | var config = new ForgeConfiguration() 254 | { 255 | ClientId = "ClientId", 256 | ClientSecret = "ClientSecret" 257 | }; 258 | var sink = new Mock(MockBehavior.Strict); 259 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny())) 260 | // some artifical delay to ensure that the other thread will attempt to enter the critical section 261 | .ReturnsAsync(new HttpResponseMessage() 262 | { 263 | Content = new StringContent(JsonConvert.SerializeObject(new Dictionary { { "token_type", "Bearer" }, { "access_token", newToken }, { "expires_in", "3" } })), 264 | StatusCode = System.Net.HttpStatusCode.OK 265 | }, TweakableForgeHandler.DefaultTimeout/2 266 | ); 267 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == requestUri && r.Headers.Authorization.Parameter == newToken), It.IsAny())) 268 | .ReturnsAsync(new HttpResponseMessage() 269 | { 270 | StatusCode = System.Net.HttpStatusCode.OK 271 | }); 272 | 273 | var fh = new TweakableForgeHandler(Options.Create(config)) 274 | { 275 | InnerHandler = sink.Object 276 | }; 277 | 278 | var scope = "somescope"; 279 | 280 | //we have token but it is expired already 281 | fh.TokenCache.Add(scope, $"Bearer {cachedToken}", TimeSpan.FromSeconds(0)); 282 | 283 | //launch 2 threads to make parallel requests 284 | Func lambda = async () => 285 | { 286 | var req = new HttpRequestMessage(); 287 | req.RequestUri = requestUri; 288 | var invoker = new HttpMessageInvoker(fh); 289 | 290 | req.Options.Set(ForgeConfiguration.ScopeKey, scope); 291 | await invoker.SendAsync(req, CancellationToken.None); 292 | }; 293 | 294 | await Task.WhenAll(lambda(), lambda()); 295 | 296 | // We expect exactly one auth call 297 | sink.Protected().As().Verify(o => o.SendAsync(It.Is(r => r.RequestUri == config.AuthenticationAddress), It.IsAny()), Times.Once()); 298 | 299 | sink.VerifyAll(); 300 | } 301 | 302 | [Fact] 303 | public async Task TestUseGoodToken() 304 | { 305 | var cachedToken = "cachedToken"; 306 | var req = new HttpRequestMessage(); 307 | req.RequestUri = new Uri("http://example.com"); 308 | var config = new ForgeConfiguration() 309 | { 310 | ClientId = "ClientId", 311 | ClientSecret = "ClientSecret" 312 | }; 313 | var sink = new Mock(MockBehavior.Strict); 314 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri && r.Headers.Authorization.Parameter == cachedToken), It.IsAny())) 315 | .ReturnsAsync(new HttpResponseMessage() 316 | { 317 | StatusCode = System.Net.HttpStatusCode.OK 318 | }); 319 | 320 | var fh = new TweakableForgeHandler(Options.Create(config)) 321 | { 322 | InnerHandler = sink.Object 323 | }; 324 | 325 | var scope = "somescope"; 326 | fh.TokenCache.Add(scope, $"Bearer {cachedToken}", TimeSpan.FromSeconds(10)); 327 | 328 | var invoker = new HttpMessageInvoker(fh); 329 | 330 | req.Options.Set(ForgeConfiguration.ScopeKey, scope); 331 | var resp = await invoker.SendAsync(req, CancellationToken.None); 332 | 333 | Assert.Equal(System.Net.HttpStatusCode.OK, resp.StatusCode); 334 | 335 | // We expect exactly one network call 336 | sink.Protected().As().Verify(o => o.SendAsync(It.IsAny(), It.IsAny()), Times.Once()); 337 | 338 | sink.VerifyAll(); 339 | } 340 | 341 | [Fact] 342 | public async Task TestNoRefreshOnClientProvidedToken() 343 | { 344 | var token = "blabla"; 345 | var req = new HttpRequestMessage(); 346 | req.RequestUri = new Uri("http://example.com"); 347 | var config = new ForgeConfiguration() 348 | { 349 | ClientId = "ClientId", 350 | ClientSecret = "ClientSecret" 351 | }; 352 | var sink = new Mock(MockBehavior.Strict); 353 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri && r.Headers.Authorization.Parameter == token), It.IsAny())) 354 | .ReturnsAsync(new HttpResponseMessage() 355 | { 356 | StatusCode = System.Net.HttpStatusCode.Unauthorized 357 | }); 358 | 359 | var fh = new TweakableForgeHandler(Options.Create(config)) 360 | { 361 | InnerHandler = sink.Object 362 | }; 363 | 364 | var scope = "somescope"; 365 | 366 | var invoker = new HttpMessageInvoker(fh); 367 | 368 | req.Options.Set(ForgeConfiguration.ScopeKey, scope); 369 | req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); 370 | var resp = await invoker.SendAsync(req, CancellationToken.None); 371 | 372 | Assert.Equal(System.Net.HttpStatusCode.Unauthorized, resp.StatusCode); 373 | 374 | // We expect exactly one network call 375 | sink.Protected().As().Verify(o => o.SendAsync(It.IsAny(), It.IsAny()), Times.Once()); 376 | 377 | sink.VerifyAll(); 378 | } 379 | } 380 | 381 | // put time consuming tests into separate classes so they are executed in parallel 382 | public class TestForgeHandler2 383 | { 384 | [Fact] 385 | public async Task TestCorrectNumberOfRetries() 386 | { 387 | var cachedToken = "cachedToken"; 388 | var req = new HttpRequestMessage(); 389 | req.RequestUri = new Uri("http://example.com"); 390 | var config = new ForgeConfiguration() 391 | { 392 | ClientId = "ClientId", 393 | ClientSecret = "ClientSecret" 394 | }; 395 | 396 | var gatewayTimeout = new HttpResponseMessage() { StatusCode = System.Net.HttpStatusCode.GatewayTimeout }; 397 | var tooManyRequests = new HttpResponseMessage { StatusCode = (System.Net.HttpStatusCode)429 }; 398 | tooManyRequests.Headers.RetryAfter = new System.Net.Http.Headers.RetryConditionHeaderValue(TimeSpan.FromSeconds(2)); 399 | var sink = new Mock(MockBehavior.Strict); 400 | sink.Protected().As().SetupSequence(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri && r.Headers.Authorization.Parameter == cachedToken), It.IsAny())) 401 | .ReturnsAsync(tooManyRequests) 402 | .ReturnsAsync(tooManyRequests) 403 | .ReturnsAsync(tooManyRequests) 404 | .ThrowsAsync(new HttpRequestException()) 405 | .ReturnsAsync(gatewayTimeout) 406 | .ReturnsAsync(gatewayTimeout); 407 | 408 | 409 | var fh = new TweakableForgeHandler(Options.Create(config)) 410 | { 411 | InnerHandler = sink.Object 412 | }; 413 | 414 | var scope = "somescope"; 415 | fh.TokenCache.Add(scope, $"Bearer {cachedToken}", TimeSpan.FromSeconds(10)); 416 | 417 | var invoker = new HttpMessageInvoker(fh); 418 | 419 | req.Options.Set(ForgeConfiguration.ScopeKey, scope); 420 | var resp = await invoker.SendAsync(req, CancellationToken.None); 421 | 422 | Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, resp.StatusCode); 423 | 424 | // We retry 5 times so expect 6 calls 425 | sink.Protected().As().Verify(o => o.SendAsync(It.IsAny(), It.IsAny()), Times.Exactly(6)); 426 | 427 | sink.VerifyAll(); 428 | } 429 | } 430 | 431 | public class TestForgeHandler3 432 | { 433 | [Fact] 434 | public async Task TestTimeout() 435 | { 436 | var cachedToken = "cachedToken"; 437 | var req = new HttpRequestMessage(); 438 | req.RequestUri = new Uri("http://example.com"); 439 | var config = new ForgeConfiguration() 440 | { 441 | ClientId = "ClientId", 442 | ClientSecret = "ClientSecret" 443 | }; 444 | var sink = new Mock(MockBehavior.Strict); 445 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri && r.Headers.Authorization.Parameter == cachedToken), It.IsAny())) 446 | .Returns(async (HttpRequestMessage r, CancellationToken ct) => 447 | { 448 | await Task.Delay(TweakableForgeHandler.DefaultTimeout*2); 449 | ct.ThrowIfCancellationRequested(); 450 | return new HttpResponseMessage() { StatusCode = System.Net.HttpStatusCode.OK }; 451 | }); 452 | 453 | var fh = new TweakableForgeHandler(Options.Create(config)) 454 | { 455 | InnerHandler = sink.Object 456 | }; 457 | 458 | var scope = "somescope"; 459 | fh.TokenCache.Add(scope, $"Bearer {cachedToken}", TimeSpan.FromSeconds(10)); 460 | 461 | var invoker = new HttpMessageInvoker(fh); 462 | 463 | req.Options.Set(ForgeConfiguration.ScopeKey, scope); 464 | await Assert.ThrowsAsync(async () => await invoker.SendAsync(req, new CancellationToken())); 465 | 466 | sink.VerifyAll(); 467 | } 468 | } 469 | 470 | public class TestForgeHandler4 471 | { 472 | [Fact] 473 | public async Task TestCircuitBreaker() 474 | { 475 | var cachedToken = "cachedToken"; 476 | var req = new HttpRequestMessage(); 477 | req.RequestUri = new Uri("http://example.com"); 478 | var config = new ForgeConfiguration() 479 | { 480 | ClientId = "ClientId", 481 | ClientSecret = "ClientSecret" 482 | }; 483 | var sink = new Mock(MockBehavior.Strict); 484 | sink.Protected().As().Setup(o => o.SendAsync(It.Is(r => r.RequestUri == req.RequestUri && r.Headers.Authorization.Parameter == cachedToken), It.IsAny())) 485 | .ReturnsAsync(new HttpResponseMessage() 486 | { 487 | StatusCode = System.Net.HttpStatusCode.InternalServerError 488 | }); 489 | 490 | var fh = new TweakableForgeHandler(Options.Create(config)) 491 | { 492 | InnerHandler = sink.Object 493 | }; 494 | 495 | var scope = "somescope"; 496 | fh.TokenCache.Add(scope, $"Bearer {cachedToken}", TimeSpan.FromSeconds(10)); 497 | 498 | var invoker = new HttpMessageInvoker(fh); 499 | 500 | req.Options.Set(ForgeConfiguration.ScopeKey, scope); 501 | // We tolerate 3 failures before we break the circuit 502 | for (int i = 0; i < 3; i++) 503 | { 504 | var resp = await invoker.SendAsync(req, CancellationToken.None); 505 | Assert.Equal(System.Net.HttpStatusCode.InternalServerError, resp.StatusCode); 506 | } 507 | 508 | await Assert.ThrowsAsync>(async () => await invoker.SendAsync(req, CancellationToken.None)); 509 | 510 | 511 | sink.Protected().As().Verify(o => o.SendAsync(It.IsAny(), It.IsAny()), Times.Exactly(3)); 512 | 513 | sink.VerifyAll(); 514 | } 515 | } 516 | 517 | /// 518 | /// Unit tests for custom timeout. 519 | /// 520 | public class TestCustomTimeout 521 | { 522 | private const string CachedToken = "cachedToken"; 523 | private const string Scope = "somescope"; 524 | 525 | private readonly ForgeConfiguration _forgeConfig = new ForgeConfiguration 526 | { 527 | ClientId = "ClientId", 528 | ClientSecret = "ClientSecret" 529 | }; 530 | 531 | [Fact] 532 | public async Task TestTriggeredTimeout() 533 | { 534 | var (sink, requestSender) = GetReady(1, TimeSpan.FromMilliseconds(1100)); 535 | await Assert.ThrowsAsync(async () => await requestSender()); 536 | 537 | sink.VerifyAll(); 538 | } 539 | 540 | [Fact] 541 | public async Task TestNoTimeout() 542 | { 543 | var (sink, requestSender) = GetReady(1, TimeSpan.FromMilliseconds(100)); 544 | 545 | HttpResponseMessage response = await requestSender(); 546 | Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); 547 | 548 | sink.VerifyAll(); 549 | } 550 | 551 | /// 552 | /// Create all required components for custom timeout validation. 553 | /// 554 | /// Allowed time in seconds. 555 | /// Actual response time. 556 | /// 557 | /// Tuple with: 558 | /// * mock to validate after tests are complete. 559 | /// * functor to perform mocked HTTP request/response operation. 560 | /// 561 | private (Mock sink, Func> requestSender) GetReady(int allowedTimeInSec, TimeSpan responseTime) 562 | { 563 | var req = RequestWithTimeout(allowedTimeInSec); 564 | var sink = MakeSink(req, responseTime); 565 | 566 | var fh = new TweakableForgeHandler(Options.Create(_forgeConfig)) 567 | { 568 | InnerHandler = sink.Object 569 | }; 570 | fh.TokenCache.Add(Scope, $"Bearer {CachedToken}", TimeSpan.FromSeconds(10)); 571 | 572 | var invoker = new HttpMessageInvoker(fh); 573 | return (sink, () => invoker.SendAsync(req, new CancellationToken())); 574 | } 575 | 576 | /// 577 | /// Create mocked HTTP message handler, who emulates timeout. 578 | /// 579 | /// Expected HTTP request. 580 | /// Response timeout in seconds. 581 | private static Mock MakeSink(HttpRequestMessage req, TimeSpan responseTime) 582 | { 583 | var sink = new Mock(MockBehavior.Strict); 584 | sink.Protected() 585 | .As() 586 | .Setup(o => o.SendAsync(It.Is(EnsureRequest(req)), It.IsAny())) 587 | .Returns(async (HttpRequestMessage r, CancellationToken ct) => 588 | { 589 | await Task.Delay(responseTime); 590 | ct.ThrowIfCancellationRequested(); 591 | return new HttpResponseMessage() { StatusCode = System.Net.HttpStatusCode.OK }; 592 | }); 593 | 594 | return sink; 595 | } 596 | 597 | private static Expression> EnsureRequest(HttpRequestMessage expected) 598 | { 599 | return (HttpRequestMessage actual) => (actual.RequestUri == expected.RequestUri) && 600 | (actual.Headers.Authorization.Parameter == CachedToken); 601 | } 602 | 603 | /// 604 | /// Create HTTP request message with custom timeout. 605 | /// 606 | /// Timeout in seconds. 607 | private static HttpRequestMessage RequestWithTimeout(int timeout) 608 | { 609 | var req = new HttpRequestMessage 610 | { 611 | RequestUri = new Uri("http://example.com") 612 | }; 613 | 614 | req.Options.Set(ForgeConfiguration.ScopeKey, Scope); 615 | req.Options.Set(ForgeConfiguration.TimeoutKey, timeout); 616 | 617 | return req; 618 | } 619 | } 620 | 621 | } 622 | -------------------------------------------------------------------------------- /tests/Autodesk.Forge.Core.Test/TestForgeService.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Autodesk.Forge.Core.Test 4 | { 5 | public class TestForgeService 6 | { 7 | [Fact] 8 | public void TestDefault() 9 | { 10 | var svc = ForgeService.CreateDefault(); 11 | Assert.NotNull(svc); 12 | Assert.NotNull(svc.Client); 13 | } 14 | 15 | [Fact] 16 | public void TestNullClientThrows() 17 | { 18 | Assert.Throws(() =>new ForgeService(null)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Autodesk.Forge.Core.Test/TestMarshalling.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Xunit; 3 | 4 | namespace Autodesk.Forge.Core.Test 5 | { 6 | public class TestMarshalling 7 | { 8 | [Fact] 9 | public async Task TestDeserializeThrowsOnNull() 10 | { 11 | await Assert.ThrowsAsync(() => Marshalling.DeserializeAsync(null)); 12 | } 13 | 14 | [Fact] 15 | public async Task TestDeserializeNonJsonThrows() 16 | { 17 | await Assert.ThrowsAsync(() => Marshalling.DeserializeAsync(new ByteArrayContent(new byte[] { 0, 2, 3 }))); 18 | } 19 | 20 | [Fact] 21 | public async Task TestDeserializeValidString() 22 | { 23 | var ret = await Marshalling.DeserializeAsync(new StringContent("\"bla\"", Encoding.UTF8, "application/json")); 24 | Assert.Equal("bla", ret); 25 | } 26 | 27 | [Fact] 28 | public async Task TestDeserializeNull() 29 | { 30 | var ret = await Marshalling.DeserializeAsync(new StringContent("null", Encoding.UTF8, "application/json")); 31 | Assert.Null(ret); 32 | } 33 | 34 | [Fact] 35 | public async Task TestDeserializeNullInvalid() 36 | { 37 | await Assert.ThrowsAsync (() => Marshalling.DeserializeAsync(new StringContent("null", Encoding.UTF8, "application/json"))); 38 | } 39 | 40 | [Fact] 41 | public void TestSerializeValidString() 42 | { 43 | var content = Marshalling.Serialize("bla"); 44 | } 45 | 46 | [Fact] 47 | public void TestBuildRequestUriUnmatchedPathTemplateThrows() 48 | { 49 | Assert.Throws(() => Marshalling.BuildRequestUri("/test/{foo}/{some}", 50 | new Dictionary 51 | { 52 | { "foo", "bar"}, 53 | }, 54 | new Dictionary() 55 | )); 56 | } 57 | [Fact] 58 | public void TestBuildRequestUriValid() 59 | { 60 | var uri = Marshalling.BuildRequestUri("/test/{foo}/{some}", 61 | new Dictionary 62 | { 63 | { "foo", "bar"}, 64 | { "some", "stuff"}, 65 | }, 66 | new Dictionary 67 | { 68 | { "page", "blabla" }, 69 | { "count", "3" } 70 | } 71 | ); 72 | Assert.Equal("test/bar/stuff?page=blabla&count=3&", uri.OriginalString); 73 | } 74 | } 75 | } 76 | --------------------------------------------------------------------------------