@@ -192,39 +192,30 @@ async fn plan_picks_a_free_name_when_the_short_one_is_taken() {
192192 drop_scratch_db ( & admin, pool, & name) . await ;
193193}
194194
195- /// The real script, with a stand-in CLI that logs each call and applies the
196- /// rename. A name may hold any character but NUL, so a name holding control
197- /// characters must still yield exactly one rename, of its own channel. Like
198- /// clap, the stand-in takes a value that starts with "-" only as `--name=`.
195+ /// Runs the real script for `host` against database `db`, with `cli_body` as
196+ /// the stand-in CLI. Returns the script's output and each CLI call's
197+ /// arguments.
199198#[ cfg( unix) ]
200- #[ tokio:: test]
201- #[ ignore = "requires Postgres" ]
202- async fn script_renames_each_planned_channel_once_whatever_its_name ( ) {
203- let admin = PgPool :: connect ( & admin_url ( ) . await ) . await . unwrap ( ) ;
204- let ( pool, name) = create_scratch_db ( & admin, "listed_name_script" ) . await ;
205- let seed = Seed :: new ( & pool) . await ;
206- let [ kept, duplicate, other] = [ ( ) ; 3 ] . map ( |_| Uuid :: new_v4 ( ) ) ;
207- let tricky = format ! ( "-ops\x1e {other}\x1f owned\x1f \n \t \" $(x)" ) ;
208- seed. open ( & pool, kept, & tricky, 30 ) . await ;
209- seed. open ( & pool, duplicate, & tricky, 10 ) . await ;
210- seed. open ( & pool, other, "other" , 20 ) . await ;
211-
199+ async fn run_rename_script (
200+ host : & str ,
201+ db : & str ,
202+ cli_body : & str ,
203+ ) -> ( std:: process:: Output , Vec < Vec < Vec < u8 > > > ) {
212204 let dir = std:: env:: temp_dir ( ) . join ( format ! ( "listed-name-script-{}" , Uuid :: new_v4( ) . simple( ) ) ) ;
213205 std:: fs:: create_dir ( & dir) . unwrap ( ) ;
214206 let ( cli, log) = ( dir. join ( "buzz" ) , dir. join ( "calls" ) ) ;
215207 std:: fs:: write (
216208 & cli,
217- "#!/usr/bin/env bash\n set -euo pipefail\n printf '%s\\ 0' \" $@\" >>\" $STUB_LOG\" \n \
218- [[ $3 == --channel=* && $4 == --name=* ]] || exit 2\n \
219- psql \" $DATABASE_URL\" -X -q -v ON_ERROR_STOP=1 -v id=\" ${3#*=}\" -v name=\" ${4#*=}\" <<'SQL'\n \
220- UPDATE channels SET name = :'name' WHERE id = :'id';\n SQL\n ",
209+ format ! (
210+ "#!/usr/bin/env bash\n set -euo pipefail\n printf '%s\\ 0' \" $@\" '' >>\" $STUB_LOG\" \n {cli_body}"
211+ ) ,
221212 )
222213 . unwrap ( ) ;
223214 use std:: os:: unix:: fs:: PermissionsExt ;
224215 std:: fs:: set_permissions ( & cli, std:: fs:: Permissions :: from_mode ( 0o755 ) ) . unwrap ( ) ;
225216
226217 let base = admin_url ( ) . await ;
227- let database_url = format ! ( "{}/{}" , & base[ ..base. rfind( '/' ) . unwrap( ) ] , name ) ;
218+ let database_url = format ! ( "{}/{}" , & base[ ..base. rfind( '/' ) . unwrap( ) ] , db ) ;
228219 let mut path = std:: env:: var ( "PATH" ) . unwrap_or_default ( ) ;
229220 if let Ok ( bin) = std:: env:: var ( "PG_BIN_DIR" ) {
230221 path = format ! ( "{bin}:{path}" ) ;
@@ -233,40 +224,137 @@ async fn script_renames_each_planned_channel_once_whatever_its_name() {
233224 env!( "CARGO_MANIFEST_DIR" ) ,
234225 "/../../scripts/maintenance/rename_duplicate_listed_channel_names.sh"
235226 ) )
236- . arg ( & seed . host )
227+ . arg ( host)
237228 . env ( "PATH" , path)
238229 . env ( "DATABASE_URL" , & database_url)
239230 . env ( "BUZZ_PRIVATE_KEY" , "unused" )
240231 . env ( "BUZZ_CLI" , & cli)
241232 . env ( "STUB_LOG" , & log)
242233 . output ( )
243234 . expect ( "run the rename script" ) ;
235+
236+ // Each call ends with an empty field, so "\0\0" splits the calls.
237+ let calls = std:: fs:: read ( & log) . unwrap_or_default ( ) ;
238+ let calls = calls
239+ . split ( |b| * b == 0 )
240+ . collect :: < Vec < _ > > ( )
241+ . split ( |field| field. is_empty ( ) )
242+ . filter ( |call| !call. is_empty ( ) )
243+ . map ( |call| call. iter ( ) . map ( |field| field. to_vec ( ) ) . collect ( ) )
244+ . collect ( ) ;
245+ std:: fs:: remove_dir_all ( & dir) . unwrap ( ) ;
246+ ( output, calls)
247+ }
248+
249+ /// The real script, with a stand-in CLI that logs each call and applies the
250+ /// rename. A name may hold any character but NUL, so a name holding control
251+ /// characters must still yield exactly one rename, of its own channel. Like
252+ /// clap, the stand-in takes a value that starts with "-" only as `--name=`.
253+ #[ cfg( unix) ]
254+ #[ tokio:: test]
255+ #[ ignore = "requires Postgres" ]
256+ async fn script_renames_each_planned_channel_once_whatever_its_name ( ) {
257+ let admin = PgPool :: connect ( & admin_url ( ) . await ) . await . unwrap ( ) ;
258+ let ( pool, name) = create_scratch_db ( & admin, "listed_name_script" ) . await ;
259+ let seed = Seed :: new ( & pool) . await ;
260+ let [ kept, duplicate, other] = [ ( ) ; 3 ] . map ( |_| Uuid :: new_v4 ( ) ) ;
261+ let tricky = format ! ( "-ops\x1e {other}\x1f owned\x1f \n \t \" $(x)" ) ;
262+ seed. open ( & pool, kept, & tricky, 30 ) . await ;
263+ seed. open ( & pool, duplicate, & tricky, 10 ) . await ;
264+ seed. open ( & pool, other, "other" , 20 ) . await ;
265+
266+ let ( output, calls) = run_rename_script (
267+ & seed. host ,
268+ & name,
269+ "[[ $3 == --channel=* && $4 == --name=* ]] || exit 2\n \
270+ psql \" $DATABASE_URL\" -X -q -v ON_ERROR_STOP=1 -v id=\" ${3#*=}\" -v name=\" ${4#*=}\" <<'SQL'\n \
271+ UPDATE channels SET name = :'name' WHERE id = :'id';\n SQL\n ",
272+ )
273+ . await ;
244274 assert ! (
245275 output. status. success( ) ,
246276 "script failed: {}" ,
247277 String :: from_utf8_lossy( & output. stderr)
248278 ) ;
249279
250280 let new_name = renamed ( duplicate, & tricky) . 1 ;
251- let calls = std:: fs:: read ( & log) . unwrap ( ) ;
252- let args: Vec < & [ u8 ] > = calls. split ( |b| * b == 0 ) . collect ( ) ;
253- let channel = format ! ( "--channel={duplicate}" ) ;
254- let name_arg = format ! ( "--name={new_name}" ) ;
255- let expected: Vec < & [ u8 ] > = vec ! [
256- b"channels" ,
257- b"update" ,
258- channel. as_bytes( ) ,
259- name_arg. as_bytes( ) ,
260- b"" ,
261- ] ;
262- assert_eq ! ( args, expected, "one call, for the duplicate only" ) ;
281+ let expected: Vec < Vec < Vec < u8 > > > = vec ! [ vec![
282+ b"channels" . to_vec( ) ,
283+ b"update" . to_vec( ) ,
284+ format!( "--channel={duplicate}" ) . into_bytes( ) ,
285+ format!( "--name={new_name}" ) . into_bytes( ) ,
286+ ] ] ;
287+ assert_eq ! ( calls, expected, "one call, for the duplicate only" ) ;
263288 let mut names = vec ! [
264289 ( kept, tricky. clone( ) ) ,
265290 ( duplicate, new_name. clone( ) ) ,
266291 ( other, "other" . to_owned( ) ) ,
267292 ] ;
268293 names. sort ( ) ;
269294 assert_eq ! ( seed. names( & pool) . await , names) ;
270- std:: fs:: remove_dir_all ( & dir) . unwrap ( ) ;
295+ drop_scratch_db ( & admin, pool, & name) . await ;
296+ }
297+
298+ /// An auth refusal (CLI exit code 3, for example 403 relay_membership_required)
299+ /// stops the script at the first rename, with a message that names the
300+ /// membership precondition.
301+ /// Mutation: drop the exit-code-3 branch in the script → RED.
302+ #[ cfg( unix) ]
303+ #[ tokio:: test]
304+ #[ ignore = "requires Postgres" ]
305+ async fn script_stops_at_the_first_auth_refusal ( ) {
306+ let admin = PgPool :: connect ( & admin_url ( ) . await ) . await . unwrap ( ) ;
307+ let ( pool, name) = create_scratch_db ( & admin, "listed_name_script_auth" ) . await ;
308+ let seed = Seed :: new ( & pool) . await ;
309+ for ( group, age) in [ ( "first" , 30 ) , ( "first" , 10 ) , ( "second" , 30 ) , ( "second" , 10 ) ] {
310+ seed. open ( & pool, Uuid :: new_v4 ( ) , group, age) . await ;
311+ }
312+
313+ let ( output, calls) = run_rename_script ( & seed. host , & name, "exit 3\n " ) . await ;
314+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
315+ assert_eq ! ( output. status. code( ) , Some ( 1 ) , "{stderr}" ) ;
316+ assert_eq ! ( calls. len( ) , 1 , "it stops after the first refusal" ) ;
317+ assert ! ( stderr. contains( "a member of" ) , "{stderr}" ) ;
318+ drop_scratch_db ( & admin, pool, & name) . await ;
319+ }
320+
321+ /// The relay serves only an active, unarchived community, so the script stops
322+ /// before any rename for an archived community, one being deleted, or a host
323+ /// no community has, and says why.
324+ /// Mutation: drop the community check in the script → RED.
325+ #[ cfg( unix) ]
326+ #[ tokio:: test]
327+ #[ ignore = "requires Postgres" ]
328+ async fn script_stops_for_a_community_the_relay_does_not_serve ( ) {
329+ let admin = PgPool :: connect ( & admin_url ( ) . await ) . await . unwrap ( ) ;
330+ let ( pool, name) = create_scratch_db ( & admin, "listed_name_script_state" ) . await ;
331+ let archived = Seed :: new ( & pool) . await ;
332+ let deleting = Seed :: new ( & pool) . await ;
333+ for seed in [ & archived, & deleting] {
334+ seed. open ( & pool, Uuid :: new_v4 ( ) , "ops" , 30 ) . await ;
335+ seed. open ( & pool, Uuid :: new_v4 ( ) , "ops" , 10 ) . await ;
336+ }
337+ sqlx:: query ( "UPDATE communities SET archived_at = now() WHERE id = $1" )
338+ . bind ( archived. community )
339+ . execute ( & pool)
340+ . await
341+ . unwrap ( ) ;
342+ crate :: test_support:: quiesce_community_for_tests (
343+ & pool,
344+ buzz_core:: CommunityId :: from_uuid ( deleting. community ) ,
345+ )
346+ . await ;
347+
348+ for ( host, message) in [
349+ ( archived. host . as_str ( ) , "Unarchive it" ) ,
350+ ( deleting. host . as_str ( ) , "being deleted" ) ,
351+ ( "missing.example" , "no community has host" ) ,
352+ ] {
353+ let ( output, calls) = run_rename_script ( host, & name, "exit 0\n " ) . await ;
354+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
355+ assert_eq ! ( output. status. code( ) , Some ( 1 ) , "{host}: {stderr}" ) ;
356+ assert ! ( calls. is_empty( ) , "{host}: no rename" ) ;
357+ assert ! ( stderr. contains( message) , "{host}: {stderr}" ) ;
358+ }
271359 drop_scratch_db ( & admin, pool, & name) . await ;
272360}
0 commit comments