• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

unrenamed / chatd / 9973933170

17 Jul 2024 12:10PM UTC coverage: 32.813% (+16.6%) from 16.215%
9973933170

push

github

unrenamed
test: input validator

820 of 2499 relevant lines covered (32.81%)

0.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.5
/src/server/session_workflow/autocomplete.rs
1
use async_trait::async_trait;
2
use std::io::Write;
3

4
use super::handler::WorkflowHandler;
5
use super::WorkflowContext;
6

7
use crate::auth::Auth;
8
use crate::chat::{
9
    ChatRoom, Command, CommandProps, OplistCommand, OplistLoadMode, Theme, TimestampMode,
10
    WhitelistCommand, WhitelistLoadMode, CHAT_COMMANDS, NOOP_CHAT_COMMANDS, OPLIST_COMMANDS,
11
    WHITELIST_COMMANDS,
12
};
13
use crate::terminal::{CloseHandle, Terminal};
14

15
pub struct Autocomplete<H>
16
where
17
    H: Clone + Write + CloseHandle + Send,
18
{
19
    next: Option<Box<dyn WorkflowHandler<H>>>,
20
}
21

22
impl<H> Autocomplete<H>
23
where
24
    H: Clone + Write + CloseHandle + Send,
25
{
26
    pub fn new() -> Self {
×
27
        Self { next: None }
28
    }
29
}
30

31
#[async_trait]
32
impl<H> WorkflowHandler<H> for Autocomplete<H>
33
where
34
    H: Clone + Write + CloseHandle + Send,
35
{
36
    #[allow(unused_variables)]
37
    async fn handle(
38
        &mut self,
39
        context: &mut WorkflowContext,
40
        terminal: &mut Terminal<H>,
41
        room: &mut ChatRoom,
42
        auth: &mut Auth,
43
    ) -> anyhow::Result<()> {
44
        let input_str = terminal.input.to_string();
3✔
45
        if input_str.trim().is_empty() {
6✔
46
            return Ok(());
1✔
47
        }
48

49
        let cursor_pos = terminal.input.cursor_byte_pos();
6✔
50
        if cursor_pos == 0 {
3✔
51
            return Ok(());
×
52
        }
53

54
        let re = regex::Regex::new(r"\S+\s*|\s+").unwrap();
3✔
55
        let mut words_iter = re.find_iter(&input_str).map(|mat| mat.as_str());
12✔
56

57
        let cmd = words_iter.next().unwrap_or(&input_str);
6✔
58
        let (cmd_prefix, cmd_end_pos, cmd_prefix_end_pos) = get_argument_details(cmd, 0);
3✔
59

60
        let commands = match auth.is_op(&context.user.public_key) {
6✔
61
            true => CHAT_COMMANDS.clone(),
2✔
62
            false => NOOP_CHAT_COMMANDS.clone(),
4✔
63
        };
64
        let complete_cmd = match commands.iter().find(|c| c.has_prefix(&cmd_prefix)) {
12✔
65
            Some(cmd) => cmd,
3✔
66
            None => return Ok(()),
1✔
67
        };
68

69
        if cursor_pos > 0 && cursor_pos <= cmd_prefix_end_pos {
6✔
70
            paste_complete_text(terminal, cmd_end_pos, &complete_cmd.cmd())?;
6✔
71
            return Ok(());
3✔
72
        }
73

74
        match complete_cmd {
1✔
75
            Command::Whitelist(_) => 'label: {
×
76
                let subcmd = words_iter.next().unwrap_or_default();
2✔
77
                let (subcmd_prefix, subcmd_end_pos, subcmd_prefix_end_pos) =
1✔
78
                    get_argument_details(subcmd, cmd_end_pos);
×
79

80
                let complete_subcmd = match WHITELIST_COMMANDS
3✔
81
                    .iter()
×
82
                    .find(|c| c.has_prefix(&subcmd_prefix))
4✔
83
                {
84
                    Some(cmd) => cmd,
2✔
85
                    None => break 'label,
×
86
                };
87

88
                if cursor_pos > cmd_end_pos && cursor_pos <= subcmd_prefix_end_pos {
4✔
89
                    paste_complete_text(terminal, subcmd_end_pos, &complete_subcmd.cmd())?;
2✔
90
                    break 'label;
×
91
                }
92

93
                match complete_subcmd {
2✔
94
                    WhitelistCommand::Add(_) | WhitelistCommand::Remove(_) => {
×
95
                        let mut prev_name_end_pos = subcmd_end_pos;
1✔
96
                        while let Some(name) = words_iter.next() {
3✔
97
                            let new_name_end_pos = prev_name_end_pos + name.len();
2✔
98
                            complete_argument(name, prev_name_end_pos, terminal, |prefix| {
3✔
99
                                room.find_name_by_prefix(prefix, context.user.username.as_ref())
1✔
100
                            })?;
101
                            prev_name_end_pos = new_name_end_pos;
1✔
102
                        }
103
                    }
104
                    WhitelistCommand::Load(_) => {
×
105
                        let mode = words_iter.next().unwrap_or_default();
2✔
106
                        complete_argument(mode, subcmd_end_pos, terminal, |prefix| {
2✔
107
                            WhitelistLoadMode::from_prefix(prefix)
1✔
108
                        })?;
109
                    }
110
                    _ => break 'label,
×
111
                }
112
            }
113
            Command::Oplist(_) => 'label: {
×
114
                let subcmd = words_iter.next().unwrap_or_default();
2✔
115
                let (subcmd_prefix, subcmd_end_pos, subcmd_prefix_end_pos) =
1✔
116
                    get_argument_details(subcmd, cmd_end_pos);
×
117

118
                let complete_subcmd = match OPLIST_COMMANDS
3✔
119
                    .iter()
×
120
                    .find(|c| c.has_prefix(&subcmd_prefix))
4✔
121
                {
122
                    Some(cmd) => cmd,
2✔
123
                    None => break 'label,
×
124
                };
125

126
                if cursor_pos > cmd_end_pos && cursor_pos <= subcmd_prefix_end_pos {
4✔
127
                    paste_complete_text(terminal, subcmd_end_pos, &complete_subcmd.cmd())?;
2✔
128
                    break 'label;
×
129
                }
130

131
                match complete_subcmd {
1✔
132
                    OplistCommand::Add(_) | OplistCommand::Remove(_) => {
×
133
                        let mut prev_name_end_pos = subcmd_end_pos;
1✔
134
                        while let Some(name) = words_iter.next() {
3✔
135
                            let new_name_end_pos = prev_name_end_pos + name.len();
1✔
136
                            complete_argument(name, prev_name_end_pos, terminal, |prefix| {
3✔
137
                                room.find_name_by_prefix(prefix, context.user.username.as_ref())
1✔
138
                            })?;
139
                            prev_name_end_pos = new_name_end_pos;
1✔
140
                        }
141
                    }
142
                    OplistCommand::Load(_) => {
×
143
                        let mode = words_iter.next().unwrap_or_default();
2✔
144
                        complete_argument(mode, subcmd_end_pos, terminal, |prefix| {
2✔
145
                            OplistLoadMode::from_prefix(prefix)
1✔
146
                        })?;
147
                    }
148
                    _ => {}
×
149
                }
150
            }
151
            Command::Timestamp(_) => {
×
152
                let mode = words_iter.next().unwrap_or_default();
2✔
153
                complete_argument(mode, cmd_end_pos, terminal, |prefix| {
2✔
154
                    TimestampMode::from_prefix(prefix)
1✔
155
                })?;
156
            }
157
            Command::Theme(_) => {
×
158
                let theme = words_iter.next().unwrap_or_default();
2✔
159
                complete_argument(theme, cmd_end_pos, terminal, |prefix| {
2✔
160
                    Theme::from_prefix(prefix)
1✔
161
                })?;
162
            }
163
            cmd if cmd.args().starts_with("<user>") || cmd.args().starts_with("[user]") => {
2✔
164
                let user = words_iter.next().unwrap_or_default();
2✔
165
                complete_argument(user, cmd_end_pos, terminal, |prefix| {
2✔
166
                    room.find_name_by_prefix(prefix, context.user.username.as_ref())
1✔
167
                })?;
168
            }
169
            _ => {}
×
170
        }
171

172
        Ok(())
1✔
173
    }
174

175
    fn next(&mut self) -> &mut Option<Box<dyn WorkflowHandler<H>>> {
×
176
        &mut self.next
×
177
    }
178
}
179

180
fn complete_argument<'a, F, T, H: Clone + Write + CloseHandle + Send>(
7✔
181
    arg: &str,
182
    prev_arg_end_pos: usize,
183
    terminal: &mut Terminal<H>,
184
    get_completion: F,
185
) -> anyhow::Result<()>
186
where
187
    F: Fn(&str) -> Option<T>,
188
    T: ToString,
189
{
190
    let cursor_pos = terminal.input.cursor_byte_pos();
14✔
191
    let (arg_prefix, arg_end_pos, arg_prefix_end_pos) = get_argument_details(arg, prev_arg_end_pos);
7✔
192
    if cursor_pos > prev_arg_end_pos && cursor_pos <= arg_prefix_end_pos {
21✔
193
        if let Some(complete) = get_completion(&arg_prefix).map(|c| c.to_string()) {
28✔
194
            paste_complete_text(terminal, arg_end_pos, &complete)?;
21✔
195
        }
196
    }
197

198
    Ok(())
7✔
199
}
200

201
fn get_argument_details(arg: &str, prev_arg_end_pos: usize) -> (String, usize, usize) {
3✔
202
    let arg_prefix = arg.trim().to_string();
3✔
203
    let arg_end_pos = prev_arg_end_pos + arg.len();
6✔
204
    let whitespace_count = arg.chars().filter(|&c| c.is_whitespace()).count();
12✔
205
    let arg_prefix_end_pos = arg_end_pos - whitespace_count;
3✔
206
    (arg_prefix, arg_end_pos, arg_prefix_end_pos)
3✔
207
}
208

209
fn paste_complete_text<H: Clone + Write + CloseHandle + Send>(
3✔
210
    terminal: &mut Terminal<H>,
211
    end_pos: usize,
212
    text: &str,
213
) -> anyhow::Result<()> {
214
    terminal.input.move_cursor_to(end_pos);
3✔
215
    terminal.input.remove_last_word_before_cursor();
3✔
216
    terminal.input.insert_before_cursor(text.as_bytes());
3✔
217
    terminal.input.insert_before_cursor(" ".as_bytes());
3✔
218
    terminal.print_input_line()?;
6✔
219
    Ok(())
3✔
220
}
221

222
#[cfg(test)]
223
mod should {
224
    use crate::{chat::User, pubkey::PubKey};
225
    use mockall::mock;
226
    use tokio::sync::mpsc;
227
    use tokio::sync::watch;
228

229
    use super::*;
230

231
    mock! {
232
        pub Handle {}
233

234
        impl Write for Handle {
235
            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize>;
236
            fn flush(&mut self) -> std::io::Result<()>;
237
        }
238

239
        impl Clone for Handle {
240
            fn clone(&self) -> Self;
241
        }
242

243
        impl CloseHandle for Handle {
244
            fn close(&mut self) {}
245
        }
246
    }
247

248
    macro_rules! setup {
249
        () => {{
250
            let user = User::default();
251
            let auth = Auth::default();
252
            let terminal = Terminal::new(MockHandle::new());
253
            let room = ChatRoom::new("Hello Chatters!");
254
            let context = WorkflowContext::new(user);
255
            let autocomplete: Autocomplete<MockHandle> = Autocomplete::new();
256
            (auth, terminal, room, context, autocomplete)
257
        }};
258
    }
259

260
    #[tokio::test]
261
    async fn return_ok() {
262
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
263
        assert!(autocomplete
264
            .handle(&mut context, &mut terminal, &mut room, &mut auth)
265
            .await
266
            .is_ok());
267
    }
268

269
    #[tokio::test]
270
    async fn complete_all_noop_commands() {
271
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
272

273
        let prefix_command_map = vec![
274
            ("/ms", "/msg"),
275
            ("/ex", "/exit"),
276
            ("/aw", "/away"),
277
            ("/ba", "/back"),
278
            ("/na", "/name"),
279
            ("/re", "/reply"),
280
            ("/fo", "/focus"),
281
            ("/us", "/users"),
282
            ("/wh", "/whois"),
283
            ("/th", "/theme"),
284
            ("/qu", "/quiet"),
285
            ("/ig", "/ignore"),
286
            ("/un", "/unignore"),
287
            ("/ti", "/timestamp"),
288
        ];
289

290
        terminal
291
            .handle()
292
            .expect_write()
293
            .times(..)
294
            .returning(|buf| Ok(buf.len()));
295

296
        terminal
297
            .handle()
298
            .expect_flush()
299
            .times(14)
300
            .returning(|| Ok(()));
301

302
        for (prefix, command) in prefix_command_map {
303
            terminal.input.clear();
304
            terminal.input.insert_before_cursor(prefix.as_bytes());
305

306
            let _ = autocomplete
307
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
308
                .await;
309

310
            assert_eq!(terminal.input.to_string(), format!("{command} "));
311
        }
312
    }
313

314
    #[tokio::test]
315
    async fn complete_all_op_commands() {
316
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
317
        auth.add_operator(context.user.public_key.clone());
318

319
        let prefix_command_map = vec![
320
            ("/ba", "/ban"),
321
            ("/mu", "/mute"),
322
            ("/ki", "/kick"),
323
            ("/mo", "/motd"),
324
            ("/bann", "/banned"),
325
            ("/op", "/oplist"),
326
            ("/whi", "/whitelist"),
327
        ];
328

329
        terminal
330
            .handle()
331
            .expect_write()
332
            .times(..)
333
            .returning(|buf| Ok(buf.len()));
334

335
        terminal
336
            .handle()
337
            .expect_flush()
338
            .times(7)
339
            .returning(|| Ok(()));
340

341
        for (prefix, command) in prefix_command_map {
342
            terminal.input.clear();
343
            terminal.input.insert_before_cursor(prefix.as_bytes());
344

345
            let _ = autocomplete
346
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
347
                .await;
348

349
            assert_eq!(terminal.input.to_string(), format!("{command} "));
350
        }
351
    }
352

353
    #[tokio::test]
354
    async fn not_complete_op_commands_when_user_is_not_operator() {
355
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
356

357
        let prefix_command_map = vec![
358
            ("/ba", "/ban"),
359
            ("/mu", "/mute"),
360
            ("/ki", "/kick"),
361
            ("/mo", "/motd"),
362
            ("/bann", "/banned"),
363
            ("/op", "/oplist"),
364
            ("/whi", "/whitelist"),
365
        ];
366

367
        terminal
368
            .handle()
369
            .expect_write()
370
            .times(..)
371
            .returning(|buf| Ok(buf.len()));
372

373
        terminal
374
            .handle()
375
            .expect_flush()
376
            .times(1)
377
            .returning(|| Ok(()));
378

379
        for (prefix, command) in prefix_command_map {
380
            terminal.input.clear();
381
            terminal.input.insert_before_cursor(prefix.as_bytes());
382

383
            let _ = autocomplete
384
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
385
                .await;
386

387
            assert_ne!(terminal.input.to_string(), format!("{command} "));
388
        }
389
    }
390

391
    #[tokio::test]
392
    async fn complete_hidden_commands() {
393
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
394

395
        let prefix_command_map = vec![
396
            ("/m", "/me"),
397
            ("/sl", "/slap"),
398
            ("/sh", "/shrug"),
399
            ("/he", "/help"),
400
            ("/ve", "/version"),
401
            ("/up", "/uptime"),
402
        ];
403

404
        terminal
405
            .handle()
406
            .expect_write()
407
            .times(..)
408
            .returning(|buf| Ok(buf.len()));
409

410
        terminal
411
            .handle()
412
            .expect_flush()
413
            .times(6)
414
            .returning(|| Ok(()));
415

416
        for (prefix, command) in prefix_command_map {
417
            terminal.input.clear();
418
            terminal.input.insert_before_cursor(prefix.as_bytes());
419

420
            let _ = autocomplete
421
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
422
                .await;
423

424
            assert_eq!(terminal.input.to_string(), format!("{command} "));
425
        }
426
    }
427

428
    #[tokio::test]
429
    async fn complete_theme_argument() {
430
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
431

432
        let prefix_full_map = vec![("mo", "mono"), ("co", "colors"), ("ha", "hacker")];
433

434
        terminal
435
            .handle()
436
            .expect_write()
437
            .times(..)
438
            .returning(|buf| Ok(buf.len()));
439

440
        terminal
441
            .handle()
442
            .expect_flush()
443
            .times(3)
444
            .returning(|| Ok(()));
445

446
        for (prefix, command) in prefix_full_map {
447
            terminal.input.clear();
448
            terminal
449
                .input
450
                .insert_before_cursor(&[b"/theme ", prefix.as_bytes()].concat());
451

452
            let _ = autocomplete
453
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
454
                .await;
455

456
            assert_eq!(terminal.input.to_string(), format!("/theme {command} "));
457
        }
458
    }
459

460
    #[tokio::test]
461
    async fn complete_timestamp_argument() {
462
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
463

464
        let prefix_full_map = vec![("ti", "time"), ("da", "datetime")];
465

466
        terminal
467
            .handle()
468
            .expect_write()
469
            .times(..)
470
            .returning(|buf| Ok(buf.len()));
471

472
        terminal
473
            .handle()
474
            .expect_flush()
475
            .times(2)
476
            .returning(|| Ok(()));
477

478
        for (prefix, command) in prefix_full_map {
479
            terminal.input.clear();
480
            terminal
481
                .input
482
                .insert_before_cursor(&[b"/timestamp ", prefix.as_bytes()].concat());
483

484
            let _ = autocomplete
485
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
486
                .await;
487

488
            assert_eq!(terminal.input.to_string(), format!("/timestamp {command} "));
489
        }
490
    }
491

492
    #[tokio::test]
493
    async fn complete_username_argument() {
494
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
495

496
        let mut alice = User::default();
497
        alice.set_username("alice".into());
498
        let mut bob = User::default();
499
        bob.set_username("bob".into());
500

501
        let (alice_msg_tx, _) = mpsc::channel(1);
502
        let (alice_exit_tx, _) = watch::channel(());
503
        room.join(
504
            1,
505
            alice.username.into(),
506
            PubKey::default(),
507
            String::default(),
508
            alice_msg_tx,
509
            alice_exit_tx,
510
        )
511
        .await
512
        .unwrap();
513

514
        let (bob_msg_tx, _) = mpsc::channel(1);
515
        let (bob_exit_tx, _) = watch::channel(());
516
        room.join(
517
            2,
518
            bob.username.into(),
519
            PubKey::default(),
520
            String::default(),
521
            bob_msg_tx,
522
            bob_exit_tx,
523
        )
524
        .await
525
        .unwrap();
526

527
        let prefix_full_map = vec![("al", "alice"), ("b", "bob")];
528

529
        terminal
530
            .handle()
531
            .expect_write()
532
            .times(..)
533
            .returning(|buf| Ok(buf.len()));
534

535
        terminal
536
            .handle()
537
            .expect_flush()
538
            .times(2)
539
            .returning(|| Ok(()));
540

541
        for (prefix, name) in prefix_full_map {
542
            terminal.input.clear();
543
            terminal
544
                .input
545
                .insert_before_cursor(&[b"/msg ", prefix.as_bytes()].concat());
546

547
            let _ = autocomplete
548
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
549
                .await;
550

551
            assert_eq!(terminal.input.to_string(), format!("/msg {name} "));
552
        }
553
    }
554

555
    #[tokio::test]
556
    async fn complete_whitelist_subcommand() {
557
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
558
        auth.add_operator(context.user.public_key.clone());
559

560
        let prefix_command_map = vec![
561
            ("o", "on"),
562
            ("of", "off"),
563
            ("a", "add"),
564
            ("re", "remove"),
565
            ("rev", "reverify"),
566
            ("s", "save"),
567
            ("l", "load"),
568
            ("st", "status"),
569
            ("h", "help"),
570
        ];
571

572
        terminal
573
            .handle()
574
            .expect_write()
575
            .times(..)
576
            .returning(|buf| Ok(buf.len()));
577

578
        terminal
579
            .handle()
580
            .expect_flush()
581
            .times(9)
582
            .returning(|| Ok(()));
583

584
        for (prefix, command) in prefix_command_map {
585
            terminal.input.clear();
586
            terminal
587
                .input
588
                .insert_before_cursor(&[b"/whitelist ", prefix.as_bytes()].concat());
589

590
            let _ = autocomplete
591
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
592
                .await;
593

594
            assert_eq!(terminal.input.to_string(), format!("/whitelist {command} "));
595
        }
596
    }
597

598
    #[tokio::test]
599
    async fn complete_oplist_subcommand() {
600
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
601
        auth.add_operator(context.user.public_key.clone());
602

603
        let prefix_command_map = vec![
604
            ("a", "add"),
605
            ("re", "remove"),
606
            ("s", "save"),
607
            ("l", "load"),
608
            ("st", "status"),
609
            ("h", "help"),
610
        ];
611

612
        terminal
613
            .handle()
614
            .expect_write()
615
            .times(..)
616
            .returning(|buf| Ok(buf.len()));
617

618
        terminal
619
            .handle()
620
            .expect_flush()
621
            .times(6)
622
            .returning(|| Ok(()));
623

624
        for (prefix, command) in prefix_command_map {
625
            terminal.input.clear();
626
            terminal
627
                .input
628
                .insert_before_cursor(&[b"/oplist ", prefix.as_bytes()].concat());
629

630
            let _ = autocomplete
631
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
632
                .await;
633

634
            assert_eq!(terminal.input.to_string(), format!("/oplist {command} "));
635
        }
636
    }
637

638
    #[tokio::test]
639
    async fn complete_whiltelist_load_command_arguments() {
640
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
641
        auth.add_operator(context.user.public_key.clone());
642

643
        let prefix_arg_map = vec![("me", "merge"), ("re", "replace")];
644

645
        terminal
646
            .handle()
647
            .expect_write()
648
            .times(..)
649
            .returning(|buf| Ok(buf.len()));
650

651
        terminal
652
            .handle()
653
            .expect_flush()
654
            .times(2)
655
            .returning(|| Ok(()));
656

657
        for (prefix, arg) in prefix_arg_map {
658
            terminal.input.clear();
659
            terminal
660
                .input
661
                .insert_before_cursor(&[b"/whitelist load ", prefix.as_bytes()].concat());
662

663
            let _ = autocomplete
664
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
665
                .await;
666

667
            assert_eq!(
668
                terminal.input.to_string(),
669
                format!("/whitelist load {arg} ")
670
            );
671
        }
672
    }
673

674
    #[tokio::test]
675
    async fn complete_oplist_load_command_arguments() {
676
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
677
        auth.add_operator(context.user.public_key.clone());
678

679
        let prefix_arg_map = vec![("me", "merge"), ("re", "replace")];
680

681
        terminal
682
            .handle()
683
            .expect_write()
684
            .times(..)
685
            .returning(|buf| Ok(buf.len()));
686

687
        terminal
688
            .handle()
689
            .expect_flush()
690
            .times(2)
691
            .returning(|| Ok(()));
692

693
        for (prefix, arg) in prefix_arg_map {
694
            terminal.input.clear();
695
            terminal
696
                .input
697
                .insert_before_cursor(&[b"/oplist load ", prefix.as_bytes()].concat());
698

699
            let _ = autocomplete
700
                .handle(&mut context, &mut terminal, &mut room, &mut auth)
701
                .await;
702

703
            assert_eq!(terminal.input.to_string(), format!("/oplist load {arg} "));
704
        }
705
    }
706

707
    #[tokio::test]
708
    async fn complete_whitelist_add_command_arguments() {
709
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
710
        auth.add_operator(context.user.public_key.clone());
711

712
        let mut alice = User::default();
713
        alice.set_username("alice".into());
714
        let mut bob = User::default();
715
        bob.set_username("bob".into());
716

717
        let (alice_msg_tx, _) = mpsc::channel(1);
718
        let (alice_exit_tx, _) = watch::channel(());
719
        room.join(
720
            1,
721
            alice.username.into(),
722
            PubKey::default(),
723
            String::default(),
724
            alice_msg_tx,
725
            alice_exit_tx,
726
        )
727
        .await
728
        .unwrap();
729

730
        let (bob_msg_tx, _) = mpsc::channel(1);
731
        let (bob_exit_tx, _) = watch::channel(());
732
        room.join(
733
            2,
734
            bob.username.into(),
735
            PubKey::default(),
736
            String::default(),
737
            bob_msg_tx,
738
            bob_exit_tx,
739
        )
740
        .await
741
        .unwrap();
742

743
        terminal
744
            .handle()
745
            .expect_write()
746
            .times(..)
747
            .returning(|buf| Ok(buf.len()));
748

749
        terminal
750
            .handle()
751
            .expect_flush()
752
            .times(2)
753
            .returning(|| Ok(()));
754

755
        terminal.input.clear();
756
        terminal.input.insert_before_cursor(b"/whitelist add al");
757
        let _ = autocomplete
758
            .handle(&mut context, &mut terminal, &mut room, &mut auth)
759
            .await;
760
        assert_eq!(terminal.input.to_string(), format!("/whitelist add alice "));
761

762
        terminal.input.insert_before_cursor(b"bo");
763
        let _ = autocomplete
764
            .handle(&mut context, &mut terminal, &mut room, &mut auth)
765
            .await;
766
        assert_eq!(
767
            terminal.input.to_string(),
768
            format!("/whitelist add alice bob ")
769
        );
770
    }
771

772
    #[tokio::test]
773
    async fn complete_oplist_add_command_arguments() {
774
        let (mut auth, mut terminal, mut room, mut context, mut autocomplete) = setup!();
775
        auth.add_operator(context.user.public_key.clone());
776

777
        let mut alice = User::default();
778
        alice.set_username("alice".into());
779
        let mut bob = User::default();
780
        bob.set_username("bob".into());
781

782
        let (alice_msg_tx, _) = mpsc::channel(1);
783
        let (alice_exit_tx, _) = watch::channel(());
784
        room.join(
785
            1,
786
            alice.username.into(),
787
            PubKey::default(),
788
            String::default(),
789
            alice_msg_tx,
790
            alice_exit_tx,
791
        )
792
        .await
793
        .unwrap();
794

795
        let (bob_msg_tx, _) = mpsc::channel(1);
796
        let (bob_exit_tx, _) = watch::channel(());
797
        room.join(
798
            2,
799
            bob.username.into(),
800
            PubKey::default(),
801
            String::default(),
802
            bob_msg_tx,
803
            bob_exit_tx,
804
        )
805
        .await
806
        .unwrap();
807

808
        terminal
809
            .handle()
810
            .expect_write()
811
            .times(..)
812
            .returning(|buf| Ok(buf.len()));
813

814
        terminal
815
            .handle()
816
            .expect_flush()
817
            .times(2)
818
            .returning(|| Ok(()));
819

820
        terminal.input.clear();
821
        terminal.input.insert_before_cursor(b"/oplist add al");
822
        let _ = autocomplete
823
            .handle(&mut context, &mut terminal, &mut room, &mut auth)
824
            .await;
825
        assert_eq!(terminal.input.to_string(), format!("/oplist add alice "));
826

827
        terminal.input.insert_before_cursor(b"bo");
828
        let _ = autocomplete
829
            .handle(&mut context, &mut terminal, &mut room, &mut auth)
830
            .await;
831
        assert_eq!(
832
            terminal.input.to_string(),
833
            format!("/oplist add alice bob ")
834
        );
835
    }
836
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc