├── .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 | [](https://www.nuget.org/packages/Autodesk.Forge.Core#readme-body-tab) [](https://www.nuget.org/packages/Autodesk.Forge.Core#supportedframeworks-body-tab) [](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 |
--------------------------------------------------------------------------------