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 along
17 * with this program. If not, see <http://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
28
29namespace
30{
31nlohmann::json providerMetadataJson( const wxString& aAuthType = wxS( "oauth2" ) )
32{
33 nlohmann::json auth = { { "type", aAuthType.ToStdString() } };
34
35 if( aAuthType == wxS( "oauth2" ) )
36 {
37 auth["metadata_url"] = "https://provider.example.test/.well-known/oauth-authorization-server";
38 auth["client_id"] = "kicad-desktop";
39 auth["scopes"] = nlohmann::json::array( { "openid", "parts.read" } );
40 }
41
42 return nlohmann::json{
43 { "provider_name", "Acme Parts" },
44 { "provider_version", "1.0.0" },
45 { "api_base_url", "https://provider.example.test/api" },
46 { "panel_url", "https://provider.example.test/app" },
47 { "session_bootstrap_url", "https://provider.example.test/session/bootstrap" },
48 { "auth", auth },
49 { "capabilities",
50 { { "web_ui_v1", true },
51 { "parts_v1", true },
52 { "direct_downloads_v1", true },
53 { "inline_payloads_v1", true } } },
54 { "max_download_bytes", 10485760 },
55 { "supported_asset_types", nlohmann::json::array( { "symbol", "footprint", "3dmodel" } ) },
56 { "parts", { { "endpoint_template", "/v1/parts/{part_id}" } } }
57 };
58}
59
60
61REMOTE_PROVIDER_METADATA parseProviderMetadata( const wxString& aAuthType = wxS( "oauth2" ) )
62{
63 wxString error;
64 std::optional<REMOTE_PROVIDER_METADATA> metadata =
65 REMOTE_PROVIDER_METADATA::FromJson( providerMetadataJson( aAuthType ), error );
66
67 if( !metadata.has_value() )
68 throw std::runtime_error( error.ToStdString() );
69
70 return *metadata;
71}
72
73
74nlohmann::json authServerMetadataJson()
75{
76 return nlohmann::json{ { "issuer", "https://provider.example.test" },
77 { "authorization_endpoint", "https://provider.example.test/oauth/authorize" },
78 { "token_endpoint", "https://provider.example.test/oauth/token" },
79 { "revocation_endpoint", "https://provider.example.test/oauth/revoke" } };
80}
81
82nlohmann::json manifestJson()
83{
84 return nlohmann::json{
85 { "part_id", "acme-res-10k" },
86 { "display_name", "10k Resistor" },
87 { "summary", "10k 0603 thick film resistor" },
88 { "license", "CC-BY-4.0" },
89 { "assets", nlohmann::json::array(
90 { { { "asset_type", "symbol" },
91 { "name", "acme-res-10k.kicad_sym" },
92 { "content_type", "application/x-kicad-symbol" },
93 { "size_bytes", 2048 },
94 { "sha256", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" },
95 { "download_url", "https://provider.example.test/downloads/acme-res-10k.kicad_sym" },
96 { "required", true } },
97 { { "asset_type", "footprint" },
98 { "name", "R_0603.pretty" },
99 { "content_type", "application/x-kicad-footprint" },
100 { "size_bytes", 4096 },
101 { "sha256", "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" },
102 { "download_url", "https://provider.example.test/downloads/R_0603.pretty" },
103 { "required", false } } } ) }
104 };
105}
106
107
108wxString dumpJson( const nlohmann::json& aJson )
109{
110 return wxString::FromUTF8( aJson.dump().c_str() );
111}
112} // namespace
113
114
115BOOST_AUTO_TEST_SUITE( RemoteProviderClientTests )
116
117BOOST_AUTO_TEST_CASE( DiscoveryFetchesWellKnownMetadata )
118{
119 std::vector<wxString> requestedUrls;
120
122 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
123 wxString& aError )
124 {
125 wxUnusedVar( aError );
126 requestedUrls.push_back( aRequest.url );
127 aResponse.status_code = 200;
128 aResponse.body = dumpJson( providerMetadataJson() );
129 return true;
130 } );
131
134
135 BOOST_REQUIRE( client.DiscoverProvider( wxString( "https://provider.example.test" ), metadata, error ) );
136 BOOST_CHECK_EQUAL( requestedUrls.size(), 1U );
137 BOOST_CHECK_EQUAL( requestedUrls.front(),
138 wxString( "https://provider.example.test/.well-known/kicad-remote-provider" ) );
139 BOOST_CHECK_EQUAL( metadata.provider_name, wxString( "Acme Parts" ) );
140}
141
142BOOST_AUTO_TEST_CASE( OAuthServerMetadataFetchParsesRfc8414Fields )
143{
144 REMOTE_PROVIDER_METADATA metadata = parseProviderMetadata();
145
147 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
148 wxString& aError )
149 {
150 wxUnusedVar( aError );
151 BOOST_CHECK_EQUAL( aRequest.url,
152 wxString( "https://provider.example.test/.well-known/oauth-authorization-server" ) );
153 aResponse.status_code = 200;
154 aResponse.body = dumpJson( authServerMetadataJson() );
155 return true;
156 } );
157
160
161 BOOST_REQUIRE( client.FetchOAuthServerMetadata( metadata, authMetadata, error ) );
162 BOOST_CHECK_EQUAL( authMetadata.authorization_endpoint,
163 wxString( "https://provider.example.test/oauth/authorize" ) );
164 BOOST_CHECK_EQUAL( authMetadata.token_endpoint, wxString( "https://provider.example.test/oauth/token" ) );
165}
166
167BOOST_AUTO_TEST_CASE( ManifestParsesDigestSizeAndDownloadUrls )
168{
169 REMOTE_PROVIDER_METADATA metadata = parseProviderMetadata( wxS( "none" ) );
170
172 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
173 wxString& aError )
174 {
175 wxUnusedVar( aError );
176 BOOST_CHECK( aRequest.method == REMOTE_PROVIDER_HTTP_METHOD::GET );
177 BOOST_CHECK_EQUAL( aRequest.url,
178 wxString( "https://provider.example.test/api/v1/parts/acme-res-10k" ) );
179 aResponse.status_code = 200;
180 aResponse.body = dumpJson( manifestJson() );
181 return true;
182 } );
183
186
187 BOOST_REQUIRE( client.FetchManifest( metadata, wxString( "acme-res-10k" ), wxString(), manifest, error ) );
188 BOOST_CHECK_EQUAL( manifest.part_id, wxString( "acme-res-10k" ) );
189 BOOST_REQUIRE_EQUAL( manifest.assets.size(), 2U );
190 BOOST_CHECK_EQUAL( manifest.assets.front().size_bytes, 2048LL );
191 BOOST_CHECK_EQUAL( manifest.assets.front().sha256,
192 wxString( "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" ) );
193 BOOST_CHECK( manifest.assets.front().required );
194}
195
196BOOST_AUTO_TEST_CASE( AuthorizationCodeExchangeParsesTokens )
197{
198 REMOTE_PROVIDER_METADATA metadata = parseProviderMetadata();
200 oauth.authorization_endpoint = wxString( "https://provider.example.test/oauth/authorize" );
201 oauth.token_endpoint = wxString( "https://provider.example.test/oauth/token" );
202
204 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
205 wxString& aError )
206 {
207 wxUnusedVar( aError );
208 BOOST_CHECK( aRequest.method == REMOTE_PROVIDER_HTTP_METHOD::POST );
209 BOOST_CHECK_EQUAL( aRequest.url, oauth.token_endpoint );
210 BOOST_CHECK( aRequest.body.Contains( wxString( "grant_type=authorization_code" ) ) );
211 BOOST_CHECK( aRequest.body.Contains( wxString( "code=test-code" ) ) );
212 aResponse.status_code = 200;
213 aResponse.body = dumpJson( {
214 { "access_token", "access-123" },
215 { "refresh_token", "refresh-123" },
216 { "token_type", "Bearer" },
217 { "scope", "openid parts.read" },
218 { "expires_in", 3600 }
219 } );
220 return true;
221 } );
222
223 OAUTH_SESSION session;
224 session.client_id = metadata.auth.client_id;
225 session.redirect_uri = wxString( "http://127.0.0.1:9000/oauth/callback" );
226 session.code_verifier = wxString( "verifier" );
227 OAUTH_TOKEN_SET tokens;
229
230 BOOST_REQUIRE( client.ExchangeAuthorizationCode( oauth, session, wxString( "test-code" ), tokens, error ) );
231 BOOST_CHECK_EQUAL( tokens.access_token, wxString( "access-123" ) );
232 BOOST_CHECK_EQUAL( tokens.refresh_token, wxString( "refresh-123" ) );
233 BOOST_CHECK_EQUAL( tokens.token_type, wxString( "Bearer" ) );
234}
235
236BOOST_AUTO_TEST_CASE( RefreshTokenExchangeParsesTokens )
237{
239 oauth.token_endpoint = wxString( "https://provider.example.test/oauth/token" );
240
242 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
243 wxString& aError )
244 {
245 wxUnusedVar( aError );
246 BOOST_CHECK( aRequest.body.Contains( wxString( "grant_type=refresh_token" ) ) );
247 BOOST_CHECK( aRequest.body.Contains( wxString( "refresh_token=refresh-123" ) ) );
248 aResponse.status_code = 200;
249 aResponse.body = dumpJson( {
250 { "access_token", "access-456" },
251 { "refresh_token", "refresh-456" },
252 { "token_type", "Bearer" },
253 { "scope", "openid parts.read" },
254 { "expires_in", 3600 }
255 } );
256 return true;
257 } );
258
259 OAUTH_TOKEN_SET tokens;
261 BOOST_REQUIRE( client.RefreshAccessToken( oauth, wxString( "kicad-desktop" ),
262 wxString( "refresh-123" ), tokens, error ) );
263 BOOST_CHECK_EQUAL( tokens.access_token, wxString( "access-456" ) );
264 BOOST_CHECK_EQUAL( tokens.refresh_token, wxString( "refresh-456" ) );
265}
266
267BOOST_AUTO_TEST_CASE( RevokeTokenPostsToRevocationEndpoint )
268{
270 oauth.revocation_endpoint = wxString( "https://provider.example.test/oauth/revoke" );
271
273 [&]( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
274 wxString& aError )
275 {
276 wxUnusedVar( aError );
277 BOOST_CHECK( aRequest.body.Contains( wxString( "token=access-123" ) ) );
278 aResponse.status_code = 200;
279 aResponse.body = wxString( "{}" );
280 return true;
281 } );
282
284 BOOST_CHECK( client.RevokeToken( oauth, wxString( "kicad-desktop" ), wxString( "access-123" ), error ) );
285}
286
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_AUTO_TEST_SUITE(CadstarPartParser)
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")