@@ -22,7 +22,7 @@ use minijinja::context;
2222use serde:: Deserialize ;
2323use tracing:: error;
2424
25- const DOWNLOADS_PER_MONTH_LIMIT : u64 = 1000 ;
25+ pub const DOWNLOADS_PER_MONTH_LIMIT : u64 = 1000 ;
2626const AVAILABLE_AFTER : TimeDelta = TimeDelta :: hours ( 24 ) ;
2727
2828#[ derive( Debug , Deserialize , FromRequestParts , utoipa:: IntoParams ) ]
@@ -198,362 +198,3 @@ async fn has_rev_dep(conn: &mut AsyncPgConnection, crate_id: i32) -> QueryResult
198198
199199 Ok ( rev_dep. is_some ( ) )
200200}
201-
202- #[ cfg( test) ]
203- mod tests {
204- use super :: * ;
205- use crate :: models:: OwnerKind ;
206- use crate :: tests:: builders:: { DependencyBuilder , PublishBuilder } ;
207- use crate :: tests:: util:: { RequestHelper , Response , TestApp } ;
208- use axum:: RequestPartsExt ;
209- use claims:: { assert_none, assert_some} ;
210- use crates_io_database:: schema:: crate_owners;
211- use diesel_async:: AsyncPgConnection ;
212- use http:: { Request , StatusCode } ;
213- use insta:: assert_snapshot;
214- use serde_json:: json;
215-
216- #[ tokio:: test]
217- async fn test_query_params ( ) -> anyhow:: Result < ( ) > {
218- let check = async |uri| {
219- let request = Request :: builder ( ) . uri ( uri) . body ( ( ) ) ?;
220- let ( mut parts, _) = request. into_parts ( ) ;
221- Ok :: < _ , anyhow:: Error > ( parts. extract :: < DeleteQueryParams > ( ) . await ?)
222- } ;
223-
224- let params = check ( "/api/v1/crates/foo" ) . await ?;
225- assert_none ! ( params. message) ;
226-
227- let params = check ( "/api/v1/crates/foo?" ) . await ?;
228- assert_none ! ( params. message) ;
229-
230- let params = check ( "/api/v1/crates/foo?message=" ) . await ?;
231- assert_eq ! ( assert_some!( params. message) , "" ) ;
232-
233- let params = check ( "/api/v1/crates/foo?message=hello%20world" ) . await ?;
234- assert_eq ! ( assert_some!( params. message) , "hello world" ) ;
235-
236- Ok ( ( ) )
237- }
238-
239- #[ tokio:: test( flavor = "multi_thread" ) ]
240- async fn test_happy_path_new_crate ( ) -> anyhow:: Result < ( ) > {
241- let ( app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
242- let mut conn = app. db_conn ( ) . await ;
243- let upstream = app. upstream_index ( ) ;
244-
245- publish_crate ( & user, "foo" ) . await ;
246- let crate_id = adjust_creation_date ( & mut conn, "foo" , 71 ) . await ?;
247-
248- // Update downloads count so that it wouldn't be deletable if it wasn't new
249- adjust_downloads ( & mut conn, crate_id, DOWNLOADS_PER_MONTH_LIMIT * 100 ) . await ?;
250-
251- assert_crate_exists ( & anon, "foo" , true ) . await ;
252- assert ! ( upstream. crate_exists( "foo" ) ?) ;
253- assert_snapshot ! ( app. stored_files( ) . await . join( "\n " ) , @r"
254- crates/foo/foo-1.0.0.crate
255- index/3/f/foo
256- rss/crates.xml
257- rss/crates/foo.xml
258- rss/updates.xml
259- " ) ;
260-
261- let response = delete_crate ( & user, "foo" ) . await ;
262- assert_snapshot ! ( response. status( ) , @"204 No Content" ) ;
263- assert ! ( response. body( ) . is_empty( ) ) ;
264-
265- assert_snapshot ! ( app. emails_snapshot( ) . await ) ;
266-
267- // Assert that the crate no longer exists
268- assert_crate_exists ( & anon, "foo" , false ) . await ;
269- assert ! ( !upstream. crate_exists( "foo" ) ?) ;
270- assert_snapshot ! ( app. stored_files( ) . await . join( "\n " ) , @r"
271- rss/crates.xml
272- rss/updates.xml
273- " ) ;
274-
275- Ok ( ( ) )
276- }
277-
278- #[ tokio:: test( flavor = "multi_thread" ) ]
279- async fn test_happy_path_old_crate ( ) -> anyhow:: Result < ( ) > {
280- let ( app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
281- let mut conn = app. db_conn ( ) . await ;
282- let upstream = app. upstream_index ( ) ;
283-
284- publish_crate ( & user, "foo" ) . await ;
285- let crate_id = adjust_creation_date ( & mut conn, "foo" , 73 ) . await ?;
286- adjust_downloads ( & mut conn, crate_id, DOWNLOADS_PER_MONTH_LIMIT ) . await ?;
287-
288- assert_crate_exists ( & anon, "foo" , true ) . await ;
289- assert ! ( upstream. crate_exists( "foo" ) ?) ;
290- assert_snapshot ! ( app. stored_files( ) . await . join( "\n " ) , @r"
291- crates/foo/foo-1.0.0.crate
292- index/3/f/foo
293- rss/crates.xml
294- rss/crates/foo.xml
295- rss/updates.xml
296- " ) ;
297-
298- let response = delete_crate ( & user, "foo" ) . await ;
299- assert_snapshot ! ( response. status( ) , @"204 No Content" ) ;
300- assert ! ( response. body( ) . is_empty( ) ) ;
301-
302- assert_snapshot ! ( app. emails_snapshot( ) . await ) ;
303-
304- // Assert that the crate no longer exists
305- assert_crate_exists ( & anon, "foo" , false ) . await ;
306- assert ! ( !upstream. crate_exists( "foo" ) ?) ;
307- assert_snapshot ! ( app. stored_files( ) . await . join( "\n " ) , @r"
308- rss/crates.xml
309- rss/updates.xml
310- " ) ;
311-
312- Ok ( ( ) )
313- }
314-
315- #[ tokio:: test( flavor = "multi_thread" ) ]
316- async fn test_happy_path_really_old_crate ( ) -> anyhow:: Result < ( ) > {
317- let ( app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
318- let mut conn = app. db_conn ( ) . await ;
319- let upstream = app. upstream_index ( ) ;
320-
321- publish_crate ( & user, "foo" ) . await ;
322- let crate_id = adjust_creation_date ( & mut conn, "foo" , 1000 * 24 ) . await ?;
323- adjust_downloads ( & mut conn, crate_id, 30 * DOWNLOADS_PER_MONTH_LIMIT ) . await ?;
324-
325- assert_crate_exists ( & anon, "foo" , true ) . await ;
326- assert ! ( upstream. crate_exists( "foo" ) ?) ;
327- assert_snapshot ! ( app. stored_files( ) . await . join( "\n " ) , @r"
328- crates/foo/foo-1.0.0.crate
329- index/3/f/foo
330- rss/crates.xml
331- rss/crates/foo.xml
332- rss/updates.xml
333- " ) ;
334-
335- let response = delete_crate ( & user, "foo" ) . await ;
336- assert_snapshot ! ( response. status( ) , @"204 No Content" ) ;
337- assert ! ( response. body( ) . is_empty( ) ) ;
338-
339- assert_snapshot ! ( app. emails_snapshot( ) . await ) ;
340-
341- // Assert that the crate no longer exists
342- assert_crate_exists ( & anon, "foo" , false ) . await ;
343- assert ! ( !upstream. crate_exists( "foo" ) ?) ;
344- assert_snapshot ! ( app. stored_files( ) . await . join( "\n " ) , @r"
345- rss/crates.xml
346- rss/updates.xml
347- " ) ;
348-
349- Ok ( ( ) )
350- }
351-
352- #[ tokio:: test( flavor = "multi_thread" ) ]
353- async fn test_no_auth ( ) -> anyhow:: Result < ( ) > {
354- let ( _app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
355-
356- publish_crate ( & user, "foo" ) . await ;
357-
358- let response = delete_crate ( & anon, "foo" ) . await ;
359- assert_snapshot ! ( response. status( ) , @"403 Forbidden" ) ;
360- assert_snapshot ! ( response. text( ) , @r#"{"errors":[{"detail":"this action requires authentication"}]}"# ) ;
361-
362- assert_crate_exists ( & anon, "foo" , true ) . await ;
363-
364- Ok ( ( ) )
365- }
366-
367- #[ tokio:: test( flavor = "multi_thread" ) ]
368- async fn test_token_auth ( ) -> anyhow:: Result < ( ) > {
369- let ( _app, anon, user, token) = TestApp :: full ( ) . with_token ( ) . await ;
370-
371- publish_crate ( & user, "foo" ) . await ;
372-
373- let response = delete_crate ( & token, "foo" ) . await ;
374- assert_snapshot ! ( response. status( ) , @"403 Forbidden" ) ;
375- assert_snapshot ! ( response. text( ) , @r#"{"errors":[{"detail":"this action can only be performed on the crates.io website"}]}"# ) ;
376-
377- assert_crate_exists ( & anon, "foo" , true ) . await ;
378-
379- Ok ( ( ) )
380- }
381-
382- #[ tokio:: test( flavor = "multi_thread" ) ]
383- async fn test_missing_crate ( ) -> anyhow:: Result < ( ) > {
384- let ( _app, _anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
385-
386- let response = delete_crate ( & user, "foo" ) . await ;
387- assert_snapshot ! ( response. status( ) , @"404 Not Found" ) ;
388- assert_snapshot ! ( response. text( ) , @r#"{"errors":[{"detail":"crate `foo` does not exist"}]}"# ) ;
389-
390- Ok ( ( ) )
391- }
392-
393- #[ tokio:: test( flavor = "multi_thread" ) ]
394- async fn test_not_owner ( ) -> anyhow:: Result < ( ) > {
395- let ( app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
396- let user2 = app. db_new_user ( "bar" ) . await ;
397-
398- publish_crate ( & user, "foo" ) . await ;
399-
400- let response = delete_crate ( & user2, "foo" ) . await ;
401- assert_snapshot ! ( response. status( ) , @"403 Forbidden" ) ;
402- assert_snapshot ! ( response. text( ) , @r#"{"errors":[{"detail":"only owners have permission to delete crates"}]}"# ) ;
403-
404- assert_crate_exists ( & anon, "foo" , true ) . await ;
405-
406- Ok ( ( ) )
407- }
408-
409- #[ tokio:: test( flavor = "multi_thread" ) ]
410- async fn test_team_owner ( ) -> anyhow:: Result < ( ) > {
411- let ( app, anon) = TestApp :: full ( ) . empty ( ) . await ;
412- let user = app. db_new_user ( "user-org-owner" ) . await ;
413- let user2 = app. db_new_user ( "user-one-team" ) . await ;
414-
415- publish_crate ( & user, "foo" ) . await ;
416-
417- // Add team owner
418- let body = json ! ( { "owners" : [ "github:test-org:all" ] } ) . to_string ( ) ;
419- let response = user. put :: < ( ) > ( "/api/v1/crates/foo/owners" , body) . await ;
420- assert_snapshot ! ( response. status( ) , @"200 OK" ) ;
421-
422- let response = delete_crate ( & user2, "foo" ) . await ;
423- assert_snapshot ! ( response. status( ) , @"403 Forbidden" ) ;
424- assert_snapshot ! ( response. text( ) , @r#"{"errors":[{"detail":"team members don't have permission to delete crates"}]}"# ) ;
425-
426- assert_crate_exists ( & anon, "foo" , true ) . await ;
427-
428- Ok ( ( ) )
429- }
430-
431- #[ tokio:: test( flavor = "multi_thread" ) ]
432- async fn test_too_many_owners ( ) -> anyhow:: Result < ( ) > {
433- let ( app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
434- let mut conn = app. db_conn ( ) . await ;
435- let user2 = app. db_new_user ( "bar" ) . await ;
436-
437- publish_crate ( & user, "foo" ) . await ;
438- let crate_id = adjust_creation_date ( & mut conn, "foo" , 73 ) . await ?;
439-
440- // Add another owner
441- diesel:: insert_into ( crate_owners:: table)
442- . values ( (
443- crate_owners:: crate_id. eq ( crate_id) ,
444- crate_owners:: owner_id. eq ( user2. as_model ( ) . id ) ,
445- crate_owners:: owner_kind. eq ( OwnerKind :: User ) ,
446- ) )
447- . execute ( & mut conn)
448- . await ?;
449-
450- let response = delete_crate ( & user, "foo" ) . await ;
451- assert_snapshot ! ( response. status( ) , @"422 Unprocessable Entity" ) ;
452- assert_snapshot ! ( response. text( ) , @r#"{"errors":[{"detail":"only crates with a single owner can be deleted after 72 hours"}]}"# ) ;
453-
454- assert_crate_exists ( & anon, "foo" , true ) . await ;
455-
456- Ok ( ( ) )
457- }
458-
459- #[ tokio:: test( flavor = "multi_thread" ) ]
460- async fn test_too_many_downloads ( ) -> anyhow:: Result < ( ) > {
461- let ( app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
462- let mut conn = app. db_conn ( ) . await ;
463-
464- publish_crate ( & user, "foo" ) . await ;
465- let crate_id = adjust_creation_date ( & mut conn, "foo" , 73 ) . await ?;
466- adjust_downloads ( & mut conn, crate_id, DOWNLOADS_PER_MONTH_LIMIT + 1 ) . await ?;
467-
468- let response = delete_crate ( & user, "foo" ) . await ;
469- assert_snapshot ! ( response. status( ) , @"422 Unprocessable Entity" ) ;
470- assert_snapshot ! ( response. text( ) , @r#"{"errors":[{"detail":"only crates with less than 1000 downloads per month can be deleted after 72 hours"}]}"# ) ;
471-
472- assert_crate_exists ( & anon, "foo" , true ) . await ;
473-
474- Ok ( ( ) )
475- }
476-
477- #[ tokio:: test( flavor = "multi_thread" ) ]
478- async fn test_rev_deps ( ) -> anyhow:: Result < ( ) > {
479- let ( _app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
480-
481- publish_crate ( & user, "foo" ) . await ;
482-
483- // Publish another crate
484- let pb = PublishBuilder :: new ( "bar" , "1.0.0" ) . dependency ( DependencyBuilder :: new ( "foo" ) ) ;
485- let response = user. publish_crate ( pb) . await ;
486- assert_snapshot ! ( response. status( ) , @"200 OK" ) ;
487-
488- let response = delete_crate ( & user, "foo" ) . await ;
489- assert_snapshot ! ( response. status( ) , @"422 Unprocessable Entity" ) ;
490- assert_snapshot ! ( response. text( ) , @r#"{"errors":[{"detail":"only crates without reverse dependencies can be deleted"}]}"# ) ;
491-
492- assert_crate_exists ( & anon, "foo" , true ) . await ;
493-
494- Ok ( ( ) )
495- }
496-
497- // Publishes a crate with the given name and a single `v1.0.0` version.
498- async fn publish_crate ( user : & impl RequestHelper , name : & str ) {
499- let pb = PublishBuilder :: new ( name, "1.0.0" ) ;
500- let response = user. publish_crate ( pb) . await ;
501- assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
502- }
503-
504- /// Moves the `created_at` field of a crate by the given number of hours
505- /// into the past and returns the ID of the crate.
506- async fn adjust_creation_date (
507- conn : & mut AsyncPgConnection ,
508- name : & str ,
509- hours : i64 ,
510- ) -> QueryResult < i32 > {
511- let created_at = Utc :: now ( ) - TimeDelta :: hours ( hours) ;
512- let created_at = created_at. naive_utc ( ) ;
513-
514- diesel:: update ( crates:: table)
515- . filter ( crates:: name. eq ( name) )
516- . set ( crates:: created_at. eq ( created_at) )
517- . returning ( crates:: id)
518- . get_result ( conn)
519- . await
520- }
521-
522- // Updates the download count of a crate.
523- async fn adjust_downloads (
524- conn : & mut AsyncPgConnection ,
525- crate_id : i32 ,
526- downloads : u64 ,
527- ) -> QueryResult < ( ) > {
528- let downloads = downloads. to_i64 ( ) . unwrap_or ( i64:: MAX ) ;
529-
530- diesel:: update ( crate_downloads:: table)
531- . filter ( crate_downloads:: crate_id. eq ( crate_id) )
532- . set ( crate_downloads:: downloads. eq ( downloads) )
533- . execute ( conn)
534- . await ?;
535-
536- Ok ( ( ) )
537- }
538-
539- // Performs the `DELETE` request to delete the crate, and runs any pending
540- // background jobs, then returns the response.
541- async fn delete_crate ( user : & impl RequestHelper , name : & str ) -> Response < ( ) > {
542- let url = format ! ( "/api/v1/crates/{name}" ) ;
543- let response = user. delete :: < ( ) > ( & url) . await ;
544- user. app ( ) . run_pending_background_jobs ( ) . await ;
545- response
546- }
547-
548- // Asserts that the crate with the given name exists or not.
549- async fn assert_crate_exists ( user : & impl RequestHelper , name : & str , exists : bool ) {
550- let expected_status = match exists {
551- true => StatusCode :: OK ,
552- false => StatusCode :: NOT_FOUND ,
553- } ;
554-
555- let url = format ! ( "/api/v1/crates/{name}" ) ;
556- let response = user. get :: < ( ) > ( & url) . await ;
557- assert_eq ! ( response. status( ) , expected_status) ;
558- }
559- }
0 commit comments