KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_remote_provider_client.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation; either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <boost/test/unit_test.hpp>
21
22#include <stdexcept>
23
24#include <oauth/oauth_session.h>
27#include <wx/utils.h>
28#include <wx/filename.h>
29
30
31namespace
32{
33wxFileName schemaPath()
34{
35 wxFileName schemaFile = wxFileName::DirName( wxString::FromUTF8( QA_SRC_ROOT ) );
36 schemaFile.AppendDir( wxS( "resources" ) );
37 schemaFile.AppendDir( wxS( "schemas" ) );
38 schemaFile.SetFullName( wxS( "kicad-remote-provider-metadata-v1.schema.json" ) );
39 return schemaFile;
40}
41
42
43nlohmann::json providerMetadataJson( const wxString& aAuthType = wxS( "oauth2" ) )
44{
45 nlohmann::json auth = { { "type", aAuthType.ToStdString() } };
46
47 if( aAuthType == wxS( "oauth2" ) )
48 {
49 auth["metadata_url"] = "https://provider.example.test/.well-known/oauth-authorization-server";
50 auth["client_id"] = "kicad-desktop";
51 auth["scopes"] = nlohmann::json::array( { "openid", "parts.read" } );
52 }
53
54 return nlohmann::json{
55 { "provider_name", "Acme Parts" },
56 { "provider_version", "1.0.0" },
57 { "api_base_url", "https://provider.example.test/api" },
58 { "panel_url", "https://provider.example.test/app" },
59 { "session_bootstrap_url", "https://provider.example.test/session/bootstrap" },
60 { "auth", auth },
61 { "capabilities",
62 { { "web_ui_v1", true },
63 { "parts_v1", true },
64 { "direct_downloads_v1", true },
65 { "inline_payloads_v1", true } } },
66 { "max_download_bytes", 10485760 },
67 { "supported_asset_types", nlohmann::json::array( { "symbol", "footprint", "3dmodel" } ) },
68 { "parts", { { "endpoint_template", "/v1/parts/{part_id}" } } }
69 };
70}
71
72
73REMOTE_PROVIDER_METADATA parseProviderMetadata( const wxString& aAuthType = wxS( "oauth2" ) )
74{
75 wxString error;
76 std::optional<REMOTE_PROVIDER_METADATA> metadata =
77 REMOTE_PROVIDER_METADATA::FromJson( providerMetadataJson( aAuthType ), schemaPath(), error );
78
79 if( !metadata.has_value() )
80 throw std::runtime_error( error.ToStdString() );
81
82 return *metadata;
83}
84
85
86nlohmann::json authServerMetadataJson()
87{
88 return nlohmann::json{ { "issuer", "https://provider.example.test" },
89 { "authorization_endpoint", "https://provider.example.test/oauth/authorize" },
90 { "token_endpoint", "https://provider.example.test/oauth/token" },
91 { "revocation_endpoint", "https://provider.example.test/oauth/revoke" } };
92}
93
94nlohmann::json manifestJson()
95{
96 return nlohmann::json{
97 { "part_id", "acme-res-10k" },
98 { "display_name", "10k Resistor" },
99 { "summary", "10k 0603 thick film resistor" },
100 { "license", "CC-BY-4.0" },
101 { "assets", nlohmann::json::array(
102 { { { "asset_type", "symbol" },
103 { "name", "acme-res-10k.kicad_sym" },
104 { "content_type", "application/x-kicad-symbol" },
105 { "size_bytes", 2048 },
106 { "sha256", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" },
107 { "download_url", "https://provider.example.test/downloads/acme-res-10k.kicad_sym" },
108 { "required", true } },
109 { { "asset_type", "footprint" },
110 { "name", "R_0603.pretty" },
111 { "content_type", "application/x-kicad-footprint" },
112 { "size_bytes", 4096 },
113 { "sha256", "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" },
114 { "download_url", "https://provider.example.test/downloads/R_0603.pretty" },
115 { "required", false } } } ) }
116 };
117}
118
119
120wxString dumpJson( const nlohmann::json& aJson )
121{
122 return wxString::FromUTF8( aJson.dump().c_str() );
123}
124struct BUILD_DIR_FIXTURE
125{
126 BUILD_DIR_FIXTURE()
127 {
128 m_runFromBuildDirWasSet = wxGetEnv( wxS( "KICAD_RUN_FROM_BUILD_DIR" ), &m_oldRunFromBuildDir );
129
130#if defined( __WXMAC__ ) || defined( __WXMSW__ )
131 // Bare QA binaries need the source schema directory as their data home.
132 wxUnsetEnv( wxS( "KICAD_RUN_FROM_BUILD_DIR" ) );
133
134 wxFileName resourcesDir = wxFileName::DirName( wxString::FromUTF8( QA_SRC_ROOT ) );
135 resourcesDir.AppendDir( wxS( "resources" ) );
136 m_stockDataHomeWasSet = wxGetEnv( wxS( "KICAD_STOCK_DATA_HOME" ), &m_oldStockDataHome );
137 wxSetEnv( wxS( "KICAD_STOCK_DATA_HOME" ), resourcesDir.GetPath() );
138#else
139 if( !m_runFromBuildDirWasSet )
140 wxSetEnv( wxS( "KICAD_RUN_FROM_BUILD_DIR" ), wxS( "1" ) );
141#endif
142 }
143
144 ~BUILD_DIR_FIXTURE()
145 {
146 if( m_runFromBuildDirWasSet )
147 wxSetEnv( wxS( "KICAD_RUN_FROM_BUILD_DIR" ), m_oldRunFromBuildDir );
148 else
149 wxUnsetEnv( wxS( "KICAD_RUN_FROM_BUILD_DIR" ) );
150
151#if defined( __WXMAC__ ) || defined( __WXMSW__ )
152 if( m_stockDataHomeWasSet )
153 wxSetEnv( wxS( "KICAD_STOCK_DATA_HOME" ), m_oldStockDataHome );
154 else
155 wxUnsetEnv( wxS( "KICAD_STOCK_DATA_HOME" ) );
156#endif
157 }
158
159 bool m_runFromBuildDirWasSet = false;
160 wxString m_oldRunFromBuildDir;
161#if defined( __WXMAC__ ) || defined( __WXMSW__ )
162 bool m_stockDataHomeWasSet = false;
163 wxString m_oldStockDataHome;
164#endif
165};
166
167} // namespace
168
169
170BOOST_FIXTURE_TEST_SUITE( RemoteProviderClientTests, BUILD_DIR_FIXTURE )
171
172BOOST_AUTO_TEST_CASE( DiscoveryFetchesWellKnownMetadata )
173{
174 std::vector<wxString> requestedUrls;
175
177 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
178 wxString& aError )
179 {
180 wxUnusedVar( aError );
181 requestedUrls.push_back( aRequest.url );
182 aResponse.status_code = 200;
183 aResponse.body = dumpJson( providerMetadataJson() );
184 return true;
185 } );
186
189
190 BOOST_REQUIRE( client.DiscoverProvider( wxString( "https://provider.example.test" ), metadata, error ) );
191 BOOST_CHECK_EQUAL( requestedUrls.size(), 1U );
192 BOOST_CHECK_EQUAL( requestedUrls.front(),
193 wxString( "https://provider.example.test/.well-known/kicad-remote-provider" ) );
194 BOOST_CHECK_EQUAL( metadata.provider_name, wxString( "Acme Parts" ) );
195}
196
197BOOST_AUTO_TEST_CASE( OAuthServerMetadataFetchParsesRfc8414Fields )
198{
199 REMOTE_PROVIDER_METADATA metadata = parseProviderMetadata();
200
202 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
203 wxString& aError )
204 {
205 wxUnusedVar( aError );
206 BOOST_CHECK_EQUAL( aRequest.url,
207 wxString( "https://provider.example.test/.well-known/oauth-authorization-server" ) );
208 aResponse.status_code = 200;
209 aResponse.body = dumpJson( authServerMetadataJson() );
210 return true;
211 } );
212
215
216 BOOST_REQUIRE( client.FetchOAuthServerMetadata( metadata, authMetadata, error ) );
217 BOOST_CHECK_EQUAL( authMetadata.authorization_endpoint,
218 wxString( "https://provider.example.test/oauth/authorize" ) );
219 BOOST_CHECK_EQUAL( authMetadata.token_endpoint, wxString( "https://provider.example.test/oauth/token" ) );
220}
221
222BOOST_AUTO_TEST_CASE( ManifestParsesDigestSizeAndDownloadUrls )
223{
224 REMOTE_PROVIDER_METADATA metadata = parseProviderMetadata( wxS( "none" ) );
225
227 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
228 wxString& aError )
229 {
230 wxUnusedVar( aError );
231 BOOST_CHECK( aRequest.method == REMOTE_PROVIDER_HTTP_METHOD::GET );
232 BOOST_CHECK_EQUAL( aRequest.url,
233 wxString( "https://provider.example.test/api/v1/parts/acme-res-10k" ) );
234 aResponse.status_code = 200;
235 aResponse.body = dumpJson( manifestJson() );
236 return true;
237 } );
238
241
242 BOOST_REQUIRE( client.FetchManifest( metadata, wxString( "acme-res-10k" ), wxString(), manifest, error ) );
243 BOOST_CHECK_EQUAL( manifest.part_id, wxString( "acme-res-10k" ) );
244 BOOST_REQUIRE_EQUAL( manifest.assets.size(), 2U );
245 BOOST_CHECK_EQUAL( manifest.assets.front().size_bytes, 2048LL );
246 BOOST_CHECK_EQUAL( manifest.assets.front().sha256,
247 wxString( "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" ) );
248 BOOST_CHECK( manifest.assets.front().required );
249}
250
251BOOST_AUTO_TEST_CASE( AuthorizationCodeExchangeParsesTokens )
252{
253 REMOTE_PROVIDER_METADATA metadata = parseProviderMetadata();
255 oauth.authorization_endpoint = wxString( "https://provider.example.test/oauth/authorize" );
256 oauth.token_endpoint = wxString( "https://provider.example.test/oauth/token" );
257
259 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
260 wxString& aError )
261 {
262 wxUnusedVar( aError );
263 BOOST_CHECK( aRequest.method == REMOTE_PROVIDER_HTTP_METHOD::POST );
264 BOOST_CHECK_EQUAL( aRequest.url, oauth.token_endpoint );
265 BOOST_CHECK( aRequest.body.Contains( wxString( "grant_type=authorization_code" ) ) );
266 BOOST_CHECK( aRequest.body.Contains( wxString( "code=test-code" ) ) );
267 aResponse.status_code = 200;
268 aResponse.body = dumpJson( {
269 { "access_token", "access-123" },
270 { "refresh_token", "refresh-123" },
271 { "token_type", "Bearer" },
272 { "scope", "openid parts.read" },
273 { "expires_in", 3600 }
274 } );
275 return true;
276 } );
277
278 OAUTH_SESSION session;
279 session.client_id = metadata.auth.client_id;
280 session.redirect_uri = wxString( "http://127.0.0.1:9000/oauth/callback" );
281 session.code_verifier = wxString( "verifier" );
282 OAUTH_TOKEN_SET tokens;
284
285 BOOST_REQUIRE( client.ExchangeAuthorizationCode( oauth, session, wxString( "test-code" ), tokens, error ) );
286 BOOST_CHECK_EQUAL( tokens.access_token, wxString( "access-123" ) );
287 BOOST_CHECK_EQUAL( tokens.refresh_token, wxString( "refresh-123" ) );
288 BOOST_CHECK_EQUAL( tokens.token_type, wxString( "Bearer" ) );
289}
290
291BOOST_AUTO_TEST_CASE( RefreshTokenExchangeParsesTokens )
292{
294 oauth.token_endpoint = wxString( "https://provider.example.test/oauth/token" );
295
297 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
298 wxString& aError )
299 {
300 wxUnusedVar( aError );
301 BOOST_CHECK( aRequest.body.Contains( wxString( "grant_type=refresh_token" ) ) );
302 BOOST_CHECK( aRequest.body.Contains( wxString( "refresh_token=refresh-123" ) ) );
303 aResponse.status_code = 200;
304 aResponse.body = dumpJson( {
305 { "access_token", "access-456" },
306 { "refresh_token", "refresh-456" },
307 { "token_type", "Bearer" },
308 { "scope", "openid parts.read" },
309 { "expires_in", 3600 }
310 } );
311 return true;
312 } );
313
314 OAUTH_TOKEN_SET tokens;
316 BOOST_REQUIRE( client.RefreshAccessToken( oauth, wxString( "kicad-desktop" ),
317 wxString( "refresh-123" ), tokens, error ) );
318 BOOST_CHECK_EQUAL( tokens.access_token, wxString( "access-456" ) );
319 BOOST_CHECK_EQUAL( tokens.refresh_token, wxString( "refresh-456" ) );
320}
321
322BOOST_AUTO_TEST_CASE( RevokeTokenPostsToRevocationEndpoint )
323{
325 oauth.revocation_endpoint = wxString( "https://provider.example.test/oauth/revoke" );
326
328 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
329 wxString& aError )
330 {
331 wxUnusedVar( aError );
332 BOOST_CHECK( aRequest.body.Contains( wxString( "token=access-123" ) ) );
333 aResponse.status_code = 200;
334 aResponse.body = wxString( "{}" );
335 return true;
336 } );
337
339 BOOST_CHECK( client.RevokeToken( oauth, wxString( "kicad-desktop" ), wxString( "access-123" ), error ) );
340}
341
wxString client_id
REMOTE_PROVIDER_HTTP_METHOD method
static std::optional< REMOTE_PROVIDER_METADATA > FromJson(const nlohmann::json &aJson, wxString &aError)
REMOTE_PROVIDER_AUTH_METADATA auth
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(DiscoveryFetchesWellKnownMetadata)
BOOST_CHECK_EQUAL(result, "25.4")