@@ -50,7 +50,7 @@ pub use secret::Secret;
5050use stdio:: stdin_stdout_to_console;
5151
5252/// Message sent by the credential helper on startup
53- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
53+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
5454pub struct CredentialHello {
5555 // Protocol versions supported by the credential process.
5656 pub v : Vec < u32 > ,
@@ -70,7 +70,7 @@ impl Credential for UnsupportedCredential {
7070}
7171
7272/// Message sent by Cargo to the credential helper after the hello
73- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
73+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
7474#[ serde( rename_all = "kebab-case" ) ]
7575pub struct CredentialRequest < ' a > {
7676 // Cargo will respond with the highest common protocol supported by both.
@@ -84,7 +84,7 @@ pub struct CredentialRequest<'a> {
8484 pub args : Vec < & ' a str > ,
8585}
8686
87- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
87+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
8888#[ serde( rename_all = "kebab-case" ) ]
8989pub struct RegistryInfo < ' a > {
9090 /// Registry index url
@@ -98,7 +98,7 @@ pub struct RegistryInfo<'a> {
9898 pub headers : Vec < String > ,
9999}
100100
101- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
101+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
102102#[ non_exhaustive]
103103#[ serde( tag = "kind" , rename_all = "kebab-case" ) ]
104104pub enum Action < ' a > {
@@ -121,7 +121,7 @@ impl<'a> Display for Action<'a> {
121121 }
122122}
123123
124- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
124+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
125125#[ serde( rename_all = "kebab-case" ) ]
126126pub struct LoginOptions < ' a > {
127127 /// Token passed on the command line via --token or from stdin
@@ -133,7 +133,7 @@ pub struct LoginOptions<'a> {
133133}
134134
135135/// A record of what kind of operation is happening that we should generate a token for.
136- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
136+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
137137#[ non_exhaustive]
138138#[ serde( tag = "operation" , rename_all = "kebab-case" ) ]
139139pub enum Operation < ' a > {
@@ -172,12 +172,13 @@ pub enum Operation<'a> {
172172}
173173
174174/// Message sent by the credential helper
175- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
175+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
176176#[ serde( tag = "kind" , rename_all = "kebab-case" ) ]
177177#[ non_exhaustive]
178178pub enum CredentialResponse {
179179 Get {
180180 token : Secret < String > ,
181+ #[ serde( flatten) ]
181182 cache : CacheControl ,
182183 operation_independent : bool ,
183184 } ,
@@ -187,14 +188,17 @@ pub enum CredentialResponse {
187188 Unknown ,
188189}
189190
190- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
191- #[ serde( rename_all = "kebab-case" ) ]
191+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
192+ #[ serde( tag = "cache" , rename_all = "kebab-case" ) ]
192193#[ non_exhaustive]
193194pub enum CacheControl {
194195 /// Do not cache this result.
195196 Never ,
196197 /// Cache this result and use it for subsequent requests in the current Cargo invocation until the specified time.
197- Expires ( #[ serde( with = "time::serde::timestamp" ) ] OffsetDateTime ) ,
198+ Expires {
199+ #[ serde( with = "time::serde::timestamp" ) ]
200+ expiration : OffsetDateTime ,
201+ } ,
198202 /// Cache this result and use it for all subsequent requests in the current Cargo invocation.
199203 Session ,
200204 #[ serde( other) ]
@@ -242,11 +246,7 @@ fn doit(
242246 if len == 0 {
243247 return Ok ( ( ) ) ;
244248 }
245- let request: CredentialRequest = serde_json:: from_str ( & buffer) ?;
246- if request. v != PROTOCOL_VERSION_1 {
247- return Err ( format ! ( "unsupported protocol version {}" , request. v) . into ( ) ) ;
248- }
249-
249+ let request = deserialize_request ( & buffer) ?;
250250 let response = stdin_stdout_to_console ( || {
251251 credential. perform ( & request. registry , & request. action , & request. args )
252252 } ) ?;
@@ -256,6 +256,17 @@ fn doit(
256256 }
257257}
258258
259+ /// Deserialize a request from Cargo.
260+ fn deserialize_request (
261+ value : & str ,
262+ ) -> Result < CredentialRequest < ' _ > , Box < dyn std:: error:: Error + Send + Sync > > {
263+ let request: CredentialRequest = serde_json:: from_str ( & value) ?;
264+ if request. v != PROTOCOL_VERSION_1 {
265+ return Err ( format ! ( "unsupported protocol version {}" , request. v) . into ( ) ) ;
266+ }
267+ Ok ( request)
268+ }
269+
259270/// Read a line of text from stdin.
260271pub fn read_line ( ) -> Result < String , io:: Error > {
261272 let mut buf = String :: new ( ) ;
@@ -282,3 +293,142 @@ pub fn read_token(
282293
283294 Ok ( Secret :: from ( read_line ( ) . map_err ( Box :: new) ?) )
284295}
296+
297+ #[ cfg( test) ]
298+ mod tests {
299+ use super :: * ;
300+
301+ #[ test]
302+ fn unsupported_version ( ) {
303+ // This shouldn't ever happen in practice, since the credential provider signals to Cargo which
304+ // protocol versions it supports, and Cargo should only attempt to use one of those.
305+ let msg = r#"{"v":999, "registry": {"index-url":""}, "args":[], "kind": "unexpected"}"# ;
306+ assert_eq ! (
307+ "unsupported protocol version 999" ,
308+ deserialize_request( msg) . unwrap_err( ) . to_string( )
309+ ) ;
310+ }
311+
312+ #[ test]
313+ fn cache_control ( ) {
314+ let cc = CacheControl :: Expires {
315+ expiration : OffsetDateTime :: from_unix_timestamp ( 1693928537 ) . unwrap ( ) ,
316+ } ;
317+ let json = serde_json:: to_string ( & cc) . unwrap ( ) ;
318+ assert_eq ! ( json, r#"{"cache":"expires","expiration":1693928537}"# ) ;
319+
320+ let cc = CacheControl :: Session ;
321+ let json = serde_json:: to_string ( & cc) . unwrap ( ) ;
322+ assert_eq ! ( json, r#"{"cache":"session"}"# ) ;
323+
324+ let cc: CacheControl = serde_json:: from_str ( r#"{"cache":"unknown-kind"}"# ) . unwrap ( ) ;
325+ assert_eq ! ( cc, CacheControl :: Unknown ) ;
326+
327+ assert_eq ! (
328+ "missing field `expiration`" ,
329+ serde_json:: from_str:: <CacheControl >( r#"{"cache":"expires"}"# )
330+ . unwrap_err( )
331+ . to_string( )
332+ ) ;
333+ }
334+
335+ #[ test]
336+ fn credential_response ( ) {
337+ let cr = CredentialResponse :: Get {
338+ cache : CacheControl :: Never ,
339+ operation_independent : true ,
340+ token : Secret :: from ( "value" . to_string ( ) ) ,
341+ } ;
342+ let json = serde_json:: to_string ( & cr) . unwrap ( ) ;
343+ assert_eq ! (
344+ json,
345+ r#"{"kind":"get","token":"value","cache":"never","operation_independent":true}"#
346+ ) ;
347+
348+ let cr = CredentialResponse :: Login ;
349+ let json = serde_json:: to_string ( & cr) . unwrap ( ) ;
350+ assert_eq ! ( json, r#"{"kind":"login"}"# ) ;
351+
352+ let cr: CredentialResponse =
353+ serde_json:: from_str ( r#"{"kind":"unknown-kind","extra-data":true}"# ) . unwrap ( ) ;
354+ assert_eq ! ( cr, CredentialResponse :: Unknown ) ;
355+
356+ let cr: CredentialResponse =
357+ serde_json:: from_str ( r#"{"kind":"login","extra-data":true}"# ) . unwrap ( ) ;
358+ assert_eq ! ( cr, CredentialResponse :: Login ) ;
359+
360+ let cr: CredentialResponse = serde_json:: from_str ( r#"{"kind":"get","token":"value","cache":"never","operation_independent":true,"extra-field-ignored":123}"# ) . unwrap ( ) ;
361+ assert_eq ! (
362+ cr,
363+ CredentialResponse :: Get {
364+ cache: CacheControl :: Never ,
365+ operation_independent: true ,
366+ token: Secret :: from( "value" . to_string( ) )
367+ }
368+ ) ;
369+ }
370+
371+ #[ test]
372+ fn credential_request ( ) {
373+ let get_oweners = CredentialRequest {
374+ v : PROTOCOL_VERSION_1 ,
375+ args : vec ! [ ] ,
376+ registry : RegistryInfo {
377+ index_url : "url" ,
378+ name : None ,
379+ headers : vec ! [ ] ,
380+ } ,
381+ action : Action :: Get ( Operation :: Owners { name : "pkg" } ) ,
382+ } ;
383+
384+ let json = serde_json:: to_string ( & get_oweners) . unwrap ( ) ;
385+ assert_eq ! (
386+ json,
387+ r#"{"v":1,"registry":{"index-url":"url"},"kind":"get","operation":"owners","name":"pkg"}"#
388+ ) ;
389+
390+ let cr: CredentialRequest =
391+ serde_json:: from_str ( r#"{"extra-1":true,"v":1,"registry":{"index-url":"url","extra-2":true},"kind":"get","operation":"owners","name":"pkg","args":[]}"# ) . unwrap ( ) ;
392+ assert_eq ! ( cr, get_oweners) ;
393+ }
394+
395+ #[ test]
396+ fn credential_request_logout ( ) {
397+ let unknown = CredentialRequest {
398+ v : PROTOCOL_VERSION_1 ,
399+ args : vec ! [ ] ,
400+ registry : RegistryInfo {
401+ index_url : "url" ,
402+ name : None ,
403+ headers : vec ! [ ] ,
404+ } ,
405+ action : Action :: Logout ,
406+ } ;
407+
408+ let cr: CredentialRequest = serde_json:: from_str (
409+ r#"{"v":1,"registry":{"index-url":"url"},"kind":"logout","extra-1":true,"args":[]}"# ,
410+ )
411+ . unwrap ( ) ;
412+ assert_eq ! ( cr, unknown) ;
413+ }
414+
415+ #[ test]
416+ fn credential_request_unknown ( ) {
417+ let unknown = CredentialRequest {
418+ v : PROTOCOL_VERSION_1 ,
419+ args : vec ! [ ] ,
420+ registry : RegistryInfo {
421+ index_url : "" ,
422+ name : None ,
423+ headers : vec ! [ ] ,
424+ } ,
425+ action : Action :: Unknown ,
426+ } ;
427+
428+ let cr: CredentialRequest = serde_json:: from_str (
429+ r#"{"v":1,"registry":{"index-url":""},"kind":"unexpected-1","extra-1":true,"args":[]}"# ,
430+ )
431+ . unwrap ( ) ;
432+ assert_eq ! ( cr, unknown) ;
433+ }
434+ }
0 commit comments