nimbus_cli/
main.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5mod cli;
6mod cmd;
7mod config;
8mod feature_utils;
9mod output;
10mod protocol;
11mod sources;
12mod updater;
13mod value_utils;
14mod version_utils;
15
16use anyhow::{bail, Result};
17use clap::Parser;
18use cli::{Cli, CliCommand, ExperimentArgs, OpenArgs};
19use sources::{ExperimentListSource, ExperimentSource, ManifestSource};
20use std::{ffi::OsString, path::PathBuf};
21
22pub(crate) static USER_AGENT: &str =
23    concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
24
25fn main() -> Result<()> {
26    let cmds = get_commands_from_cli(std::env::args_os())?;
27    for c in cmds {
28        let success = cmd::process_cmd(&c)?;
29        if !success {
30            bail!("Failed");
31        }
32    }
33    updater::check_for_update();
34    Ok(())
35}
36
37fn get_commands_from_cli<I, T>(args: I) -> Result<Vec<AppCommand>>
38where
39    I: IntoIterator<Item = T>,
40    T: Into<OsString> + Clone,
41{
42    let cli = Cli::try_parse_from(args)?;
43
44    let mut commands: Vec<AppCommand> = Default::default();
45
46    // We do this here to ensure that all the command line is valid
47    // with respect to the main command. We do this here because
48    // as the cli has expanded, we've changed when we need `--app`
49    // and `--channel`. We catch those types of errors early by doing this
50    // here.
51    let main_command = AppCommand::try_from(&cli)?;
52
53    // Validating the command line args. Most of this should be done with clap,
54    // but for everything else there's:
55    cli.command.check_valid()?;
56
57    // Validating experiments against manifests
58    commands.push(AppCommand::try_validate(&cli)?);
59
60    if cli.command.should_kill() {
61        let app = LaunchableApp::try_from(&cli)?;
62        commands.push(AppCommand::Kill { app });
63    }
64    if cli.command.should_reset() {
65        let app = LaunchableApp::try_from(&cli)?;
66        commands.push(AppCommand::Reset { app });
67    }
68    commands.push(main_command);
69
70    Ok(commands)
71}
72
73#[derive(Clone, Debug, PartialEq)]
74enum LaunchableApp {
75    Android {
76        package_name: String,
77        activity_name: String,
78        device_id: Option<String>,
79        scheme: Option<String>,
80        open_deeplink: Option<String>,
81    },
82    Ios {
83        device_id: String,
84        app_id: String,
85        scheme: Option<String>,
86    },
87}
88
89#[derive(Clone, Debug, PartialEq)]
90pub(crate) struct NimbusApp {
91    app_name: Option<String>,
92    channel: Option<String>,
93}
94
95impl NimbusApp {
96    #[cfg(test)]
97    fn new(app: &str, channel: &str) -> Self {
98        Self {
99            app_name: Some(app.to_string()),
100            channel: Some(channel.to_string()),
101        }
102    }
103
104    fn channel(&self) -> Option<String> {
105        self.channel.clone()
106    }
107    fn app_name(&self) -> Option<String> {
108        self.app_name.clone()
109    }
110}
111
112impl From<&Cli> for NimbusApp {
113    fn from(value: &Cli) -> Self {
114        Self {
115            channel: value.channel.clone(),
116            app_name: value.app.clone(),
117        }
118    }
119}
120
121#[derive(Debug, PartialEq)]
122enum AppCommand {
123    ApplyFile {
124        app: LaunchableApp,
125        open: AppOpenArgs,
126        list: ExperimentListSource,
127        preserve_nimbus_db: bool,
128    },
129
130    CaptureLogs {
131        app: LaunchableApp,
132        file: PathBuf,
133    },
134
135    Defaults {
136        manifest: ManifestSource,
137        feature_id: Option<String>,
138        output: Option<PathBuf>,
139    },
140
141    Enroll {
142        app: LaunchableApp,
143        params: NimbusApp,
144        experiment: ExperimentSource,
145        rollouts: Vec<ExperimentSource>,
146        branch: String,
147        preserve_targeting: bool,
148        preserve_bucketing: bool,
149        preserve_nimbus_db: bool,
150        open: AppOpenArgs,
151    },
152
153    ExtractFeatures {
154        experiment: ExperimentSource,
155        branch: String,
156        manifest: ManifestSource,
157
158        feature_id: Option<String>,
159        validate: bool,
160        multi: bool,
161
162        output: Option<PathBuf>,
163    },
164
165    FetchList {
166        list: ExperimentListSource,
167        file: Option<PathBuf>,
168    },
169
170    FmlPassthrough {
171        args: Vec<OsString>,
172        cwd: PathBuf,
173    },
174
175    Info {
176        experiment: ExperimentSource,
177        output: Option<PathBuf>,
178    },
179
180    Kill {
181        app: LaunchableApp,
182    },
183
184    List {
185        list: ExperimentListSource,
186    },
187
188    LogState {
189        app: LaunchableApp,
190        open: AppOpenArgs,
191    },
192
193    // No Op, does nothing.
194    NoOp,
195
196    Open {
197        app: LaunchableApp,
198        open: AppOpenArgs,
199    },
200
201    Reset {
202        app: LaunchableApp,
203    },
204
205    #[cfg(feature = "server")]
206    StartServer,
207
208    TailLogs {
209        app: LaunchableApp,
210    },
211
212    Unenroll {
213        app: LaunchableApp,
214        open: AppOpenArgs,
215    },
216
217    ValidateExperiment {
218        params: NimbusApp,
219        manifest: ManifestSource,
220        experiment: ExperimentSource,
221    },
222}
223
224impl AppCommand {
225    fn try_validate(cli: &Cli) -> Result<Self> {
226        let params = cli.into();
227        Ok(match &cli.command {
228            CliCommand::Enroll {
229                no_validate,
230                manifest,
231                ..
232            }
233            | CliCommand::TestFeature {
234                no_validate,
235                manifest,
236                ..
237            } if !no_validate => {
238                let experiment = ExperimentSource::try_from(cli)?;
239                let manifest = ManifestSource::try_from(&params, manifest)?;
240                AppCommand::ValidateExperiment {
241                    params,
242                    experiment,
243                    manifest,
244                }
245            }
246            CliCommand::Validate { manifest, .. } => {
247                let experiment = ExperimentSource::try_from(cli)?;
248                let manifest = ManifestSource::try_from(&params, manifest)?;
249                AppCommand::ValidateExperiment {
250                    params,
251                    experiment,
252                    manifest,
253                }
254            }
255            _ => Self::NoOp,
256        })
257    }
258}
259
260impl TryFrom<&Cli> for AppCommand {
261    type Error = anyhow::Error;
262
263    fn try_from(cli: &Cli) -> Result<Self> {
264        let params = NimbusApp::from(cli);
265        Ok(match cli.command.clone() {
266            CliCommand::ApplyFile {
267                file,
268                preserve_nimbus_db,
269                open,
270            } => {
271                let app = LaunchableApp::try_from(cli)?;
272                let list = ExperimentListSource::try_from(file.as_path())?;
273                AppCommand::ApplyFile {
274                    app,
275                    open: open.into(),
276                    list,
277                    preserve_nimbus_db,
278                }
279            }
280            CliCommand::CaptureLogs { file } => {
281                let app = LaunchableApp::try_from(cli)?;
282                AppCommand::CaptureLogs { app, file }
283            }
284            CliCommand::Defaults {
285                feature_id,
286                output,
287                manifest,
288            } => {
289                let manifest = ManifestSource::try_from(&params, &manifest)?;
290                AppCommand::Defaults {
291                    manifest,
292                    feature_id,
293                    output,
294                }
295            }
296            CliCommand::Enroll {
297                branch,
298                rollouts,
299                preserve_targeting,
300                preserve_bucketing,
301                preserve_nimbus_db,
302                experiment,
303                open,
304                ..
305            } => {
306                let app = LaunchableApp::try_from(cli)?;
307                // Ensure we get the rollouts from the same place we get the experiment from.
308                let mut recipes: Vec<ExperimentSource> = Vec::new();
309                for r in rollouts {
310                    let rollout = ExperimentArgs {
311                        experiment: r,
312                        ..experiment.clone()
313                    };
314                    recipes.push(ExperimentSource::try_from(&rollout)?);
315                }
316
317                let experiment = ExperimentSource::try_from(cli)?;
318
319                Self::Enroll {
320                    app,
321                    params,
322                    experiment,
323                    branch,
324                    rollouts: recipes,
325                    preserve_targeting,
326                    preserve_bucketing,
327                    preserve_nimbus_db,
328                    open: open.into(),
329                }
330            }
331            CliCommand::Features {
332                manifest,
333                branch,
334                feature_id,
335                output,
336                validate,
337                multi,
338                ..
339            } => {
340                let manifest = ManifestSource::try_from(&params, &manifest)?;
341                let experiment = ExperimentSource::try_from(cli)?;
342                AppCommand::ExtractFeatures {
343                    experiment,
344                    branch,
345                    manifest,
346                    feature_id,
347                    validate,
348                    multi,
349                    output,
350                }
351            }
352            CliCommand::Fetch { output, .. } | CliCommand::FetchList { output, .. } => {
353                let list = ExperimentListSource::try_from(cli)?;
354
355                AppCommand::FetchList { list, file: output }
356            }
357            CliCommand::Fml { args } => {
358                let cwd = std::env::current_dir().expect("Current Working Directory is not set");
359                AppCommand::FmlPassthrough { args, cwd }
360            }
361            CliCommand::Info { experiment, output } => AppCommand::Info {
362                experiment: ExperimentSource::try_from(&experiment)?,
363                output,
364            },
365            CliCommand::List { .. } => {
366                let list = ExperimentListSource::try_from(cli)?;
367                AppCommand::List { list }
368            }
369            CliCommand::LogState { open } => {
370                let app = LaunchableApp::try_from(cli)?;
371                AppCommand::LogState {
372                    app,
373                    open: open.into(),
374                }
375            }
376            CliCommand::Open { open, .. } => {
377                let app = LaunchableApp::try_from(cli)?;
378                AppCommand::Open {
379                    app,
380                    open: open.into(),
381                }
382            }
383            #[cfg(feature = "server")]
384            CliCommand::StartServer => AppCommand::StartServer,
385            CliCommand::TailLogs => {
386                let app = LaunchableApp::try_from(cli)?;
387                AppCommand::TailLogs { app }
388            }
389            CliCommand::TestFeature { files, open, .. } => {
390                let app = LaunchableApp::try_from(cli)?;
391                let experiment = ExperimentSource::try_from(cli)?;
392                let first = files
393                    .first()
394                    .ok_or_else(|| anyhow::Error::msg("Need at least one file to make a branch"))?;
395                let branch = feature_utils::slug(first)?;
396
397                Self::Enroll {
398                    app,
399                    params,
400                    experiment,
401                    branch,
402                    rollouts: Default::default(),
403                    open: open.into(),
404                    preserve_targeting: false,
405                    preserve_bucketing: false,
406                    preserve_nimbus_db: false,
407                }
408            }
409            CliCommand::Unenroll { open } => {
410                let app = LaunchableApp::try_from(cli)?;
411                AppCommand::Unenroll {
412                    app,
413                    open: open.into(),
414                }
415            }
416            _ => Self::NoOp,
417        })
418    }
419}
420
421impl CliCommand {
422    fn check_valid(&self) -> Result<()> {
423        // Check validity of the OpenArgs.
424        if let Some(open) = self.open_args() {
425            if open.reset_app || !open.passthrough.is_empty() {
426                const ERR: &str = "does not work with --reset-app or passthrough args";
427                if open.pbcopy {
428                    bail!(format!("{} {}", "--pbcopy", ERR));
429                }
430                if open.pbpaste {
431                    bail!(format!("{} {}", "--pbpaste", ERR));
432                }
433                if open.output.is_some() {
434                    bail!(format!("{} {}", "--output", ERR));
435                }
436            }
437            if open.deeplink.is_some() {
438                const ERR: &str = "does not work with --deeplink";
439                if open.output.is_some() {
440                    bail!(format!("{} {}", "--output", ERR));
441                }
442            }
443        }
444        Ok(())
445    }
446
447    fn open_args(&self) -> Option<&OpenArgs> {
448        if let Self::ApplyFile { open, .. }
449        | Self::Open { open, .. }
450        | Self::Enroll { open, .. }
451        | Self::LogState { open, .. }
452        | Self::TestFeature { open, .. }
453        | Self::Unenroll { open, .. } = self
454        {
455            Some(open)
456        } else {
457            None
458        }
459    }
460
461    fn should_kill(&self) -> bool {
462        if let Some(open) = self.open_args() {
463            let using_links = open.pbcopy || open.pbpaste;
464            let output_to_file = open.output.is_some();
465            let no_clobber = if let Self::Open { no_clobber, .. } = self {
466                *no_clobber
467            } else {
468                false
469            };
470            !using_links && !no_clobber && !output_to_file
471        } else {
472            matches!(self, Self::ResetApp)
473        }
474    }
475
476    fn should_reset(&self) -> bool {
477        if let Some(open) = self.open_args() {
478            open.reset_app
479        } else {
480            matches!(self, Self::ResetApp)
481        }
482    }
483}
484
485#[derive(Debug, Default, PartialEq)]
486pub(crate) struct AppOpenArgs {
487    deeplink: Option<String>,
488    passthrough: Vec<String>,
489    pbcopy: bool,
490    pbpaste: bool,
491
492    output: Option<PathBuf>,
493}
494
495impl From<OpenArgs> for AppOpenArgs {
496    fn from(value: OpenArgs) -> Self {
497        Self {
498            deeplink: value.deeplink,
499            passthrough: value.passthrough,
500            pbcopy: value.pbcopy,
501            pbpaste: value.pbpaste,
502            output: value.output,
503        }
504    }
505}
506
507impl AppOpenArgs {
508    fn args(&self) -> (&[String], &[String]) {
509        let splits = &mut self.passthrough.splitn(2, |item| item == "{}");
510        match (splits.next(), splits.next()) {
511            (Some(first), Some(last)) => (first, last),
512            (None, Some(last)) | (Some(last), None) => (&[], last),
513            _ => (&[], &[]),
514        }
515    }
516}
517
518#[cfg(test)]
519mod unit_tests {
520    use crate::sources::ExperimentListFilter;
521
522    use super::*;
523
524    #[test]
525    fn test_launchable_app() -> Result<()> {
526        fn cli(app: &str, channel: &str) -> Cli {
527            Cli {
528                app: Some(app.to_string()),
529                channel: Some(channel.to_string()),
530                device_id: None,
531                command: CliCommand::ResetApp,
532            }
533        }
534        fn android(
535            package: &str,
536            activity: &str,
537            scheme: Option<&str>,
538            open_deeplink: Option<&str>,
539        ) -> LaunchableApp {
540            LaunchableApp::Android {
541                package_name: package.to_string(),
542                activity_name: activity.to_string(),
543                device_id: None,
544                scheme: scheme.map(str::to_string),
545                open_deeplink: open_deeplink.map(str::to_string),
546            }
547        }
548        fn ios(id: &str, scheme: Option<&str>) -> LaunchableApp {
549            LaunchableApp::Ios {
550                app_id: id.to_string(),
551                device_id: "booted".to_string(),
552                scheme: scheme.map(str::to_string),
553            }
554        }
555
556        // Firefox for Android, a.k.a. fenix
557        assert_eq!(
558            LaunchableApp::try_from(&cli("fenix", "developer"))?,
559            android(
560                "org.mozilla.fenix.debug",
561                ".App",
562                Some("fenix-dev"),
563                Some("open")
564            )
565        );
566        assert_eq!(
567            LaunchableApp::try_from(&cli("fenix", "nightly"))?,
568            android(
569                "org.mozilla.fenix",
570                ".App",
571                Some("fenix-nightly"),
572                Some("open")
573            )
574        );
575        assert_eq!(
576            LaunchableApp::try_from(&cli("fenix", "beta"))?,
577            android(
578                "org.mozilla.firefox_beta",
579                ".App",
580                Some("fenix-beta"),
581                Some("open")
582            )
583        );
584        assert_eq!(
585            LaunchableApp::try_from(&cli("fenix", "release"))?,
586            android("org.mozilla.firefox", ".App", Some("fenix"), Some("open"))
587        );
588
589        // Firefox for iOS
590        assert_eq!(
591            LaunchableApp::try_from(&cli("firefox_ios", "developer"))?,
592            ios("org.mozilla.ios.Fennec", Some("fennec"))
593        );
594        assert_eq!(
595            LaunchableApp::try_from(&cli("firefox_ios", "beta"))?,
596            ios("org.mozilla.ios.FirefoxBeta", Some("firefox-beta"))
597        );
598        assert_eq!(
599            LaunchableApp::try_from(&cli("firefox_ios", "release"))?,
600            ios("org.mozilla.ios.Firefox", Some("firefox-internal"))
601        );
602
603        // Focus for Android
604        assert_eq!(
605            LaunchableApp::try_from(&cli("focus_android", "developer"))?,
606            android(
607                "org.mozilla.focus.debug",
608                "org.mozilla.focus.activity.MainActivity",
609                None,
610                None,
611            )
612        );
613        assert_eq!(
614            LaunchableApp::try_from(&cli("focus_android", "nightly"))?,
615            android(
616                "org.mozilla.focus.nightly",
617                "org.mozilla.focus.activity.MainActivity",
618                None,
619                None,
620            )
621        );
622        assert_eq!(
623            LaunchableApp::try_from(&cli("focus_android", "beta"))?,
624            android(
625                "org.mozilla.focus.beta",
626                "org.mozilla.focus.activity.MainActivity",
627                None,
628                None,
629            )
630        );
631        assert_eq!(
632            LaunchableApp::try_from(&cli("focus_android", "release"))?,
633            android(
634                "org.mozilla.focus",
635                "org.mozilla.focus.activity.MainActivity",
636                None,
637                None,
638            )
639        );
640
641        Ok(())
642    }
643
644    #[test]
645    fn test_split_args() -> Result<()> {
646        let mut open = AppOpenArgs {
647            passthrough: vec![],
648            ..Default::default()
649        };
650        let empty: &[String] = &[];
651        let expected = (empty, empty);
652        let observed = open.args();
653        assert_eq!(observed.0, expected.0);
654        assert_eq!(observed.1, expected.1);
655
656        open.passthrough = vec!["{}".to_string()];
657        let expected = (empty, empty);
658        let observed = open.args();
659        assert_eq!(observed.0, expected.0);
660        assert_eq!(observed.1, expected.1);
661
662        open.passthrough = vec!["foo".to_string(), "bar".to_string()];
663        let expected: (&[String], &[String]) = (empty, &["foo".to_string(), "bar".to_string()]);
664        let observed = open.args();
665        assert_eq!(observed.0, expected.0);
666        assert_eq!(observed.1, expected.1);
667
668        open.passthrough = vec!["foo".to_string(), "bar".to_string(), "{}".to_string()];
669        let expected: (&[String], &[String]) = (&["foo".to_string(), "bar".to_string()], empty);
670        let observed = open.args();
671        assert_eq!(observed.0, expected.0);
672        assert_eq!(observed.1, expected.1);
673
674        open.passthrough = vec!["foo".to_string(), "{}".to_string(), "bar".to_string()];
675        let expected: (&[String], &[String]) = (&["foo".to_string()], &["bar".to_string()]);
676        let observed = open.args();
677        assert_eq!(observed.0, expected.0);
678        assert_eq!(observed.1, expected.1);
679
680        open.passthrough = vec!["{}".to_string(), "foo".to_string(), "bar".to_string()];
681        let expected: (&[String], &[String]) = (empty, &["foo".to_string(), "bar".to_string()]);
682        let observed = open.args();
683        assert_eq!(observed.0, expected.0);
684        assert_eq!(observed.1, expected.1);
685
686        Ok(())
687    }
688
689    fn fenix() -> LaunchableApp {
690        LaunchableApp::Android {
691            package_name: "org.mozilla.fenix.debug".to_string(),
692            activity_name: ".App".to_string(),
693            device_id: None,
694            scheme: Some("fenix-dev".to_string()),
695            open_deeplink: Some("open".to_string()),
696        }
697    }
698
699    fn fenix_params() -> NimbusApp {
700        NimbusApp::new("fenix", "developer")
701    }
702
703    fn fenix_old_manifest_with_ref(ref_: &str) -> ManifestSource {
704        ManifestSource::FromGithub {
705            channel: "developer".into(),
706            github_repo: "mozilla-mobile/firefox-android".into(),
707            ref_: ref_.into(),
708            manifest_file: "@mozilla-mobile/firefox-android/fenix/app/nimbus.fml.yaml".into(),
709        }
710    }
711
712    fn fenix_manifest() -> ManifestSource {
713        fenix_manifest_with_ref("master")
714    }
715
716    fn fenix_manifest_with_ref(ref_: &str) -> ManifestSource {
717        ManifestSource::FromGithub {
718            github_repo: "mozilla/gecko-dev".to_string(),
719            ref_: ref_.to_string(),
720            manifest_file: "@mozilla/gecko-dev/mobile/android/fenix/app/nimbus.fml.yaml".into(),
721            channel: "developer".to_string(),
722        }
723    }
724
725    fn manifest_from_file(file: &str) -> ManifestSource {
726        ManifestSource::FromFile {
727            channel: "developer".to_string(),
728            manifest_file: file.to_string(),
729        }
730    }
731
732    fn experiment(slug: &str) -> ExperimentSource {
733        let endpoint = config::api_v6_production_server();
734        ExperimentSource::FromApiV6 {
735            slug: slug.to_string(),
736            endpoint,
737        }
738    }
739
740    fn feature_experiment(feature_id: &str, files: &[&str]) -> ExperimentSource {
741        ExperimentSource::FromFeatureFiles {
742            app: fenix_params(),
743            feature_id: feature_id.to_string(),
744            files: files.iter().map(|f| f.into()).collect(),
745        }
746    }
747
748    fn with_deeplink(link: &str) -> AppOpenArgs {
749        AppOpenArgs {
750            deeplink: Some(link.to_string()),
751            ..Default::default()
752        }
753    }
754
755    fn with_pbcopy() -> AppOpenArgs {
756        AppOpenArgs {
757            pbcopy: true,
758            ..Default::default()
759        }
760    }
761
762    fn with_passthrough(params: &[&str]) -> AppOpenArgs {
763        AppOpenArgs {
764            passthrough: params.iter().map(|s| s.to_string()).collect(),
765            ..Default::default()
766        }
767    }
768
769    fn with_output(filename: &str) -> AppOpenArgs {
770        AppOpenArgs {
771            output: Some(PathBuf::from(filename)),
772            ..Default::default()
773        }
774    }
775
776    fn for_app(app: &str, list: ExperimentListSource) -> ExperimentListSource {
777        ExperimentListSource::Filtered {
778            filter: ExperimentListFilter::for_app(app),
779            inner: Box::new(list),
780        }
781    }
782
783    fn for_feature(feature: &str, list: ExperimentListSource) -> ExperimentListSource {
784        ExperimentListSource::Filtered {
785            filter: ExperimentListFilter::for_feature(feature),
786            inner: Box::new(list),
787        }
788    }
789
790    fn for_active_on_date(date: &str, list: ExperimentListSource) -> ExperimentListSource {
791        ExperimentListSource::Filtered {
792            filter: ExperimentListFilter::for_active_on(date),
793            inner: Box::new(list),
794        }
795    }
796
797    fn for_enrolling_on_date(date: &str, list: ExperimentListSource) -> ExperimentListSource {
798        ExperimentListSource::Filtered {
799            filter: ExperimentListFilter::for_enrolling_on(date),
800            inner: Box::new(list),
801        }
802    }
803
804    #[test]
805    fn test_enroll() -> Result<()> {
806        let observed = get_commands_from_cli([
807            "nimbus-cli",
808            "--app",
809            "fenix",
810            "--channel",
811            "developer",
812            "enroll",
813            "my-experiment",
814            "--branch",
815            "my-branch",
816            "--no-validate",
817        ])?;
818
819        let expected = vec![
820            AppCommand::NoOp,
821            AppCommand::Kill { app: fenix() },
822            AppCommand::Enroll {
823                app: fenix(),
824                params: fenix_params(),
825                experiment: experiment("my-experiment"),
826                rollouts: Default::default(),
827                branch: "my-branch".to_string(),
828                preserve_targeting: false,
829                preserve_bucketing: false,
830                preserve_nimbus_db: false,
831                open: Default::default(),
832            },
833        ];
834        assert_eq!(expected, observed);
835        Ok(())
836    }
837
838    #[test]
839    fn test_enroll_with_reset_app() -> Result<()> {
840        let observed = get_commands_from_cli([
841            "nimbus-cli",
842            "--app",
843            "fenix",
844            "--channel",
845            "developer",
846            "enroll",
847            "my-experiment",
848            "--branch",
849            "my-branch",
850            "--reset-app",
851            "--no-validate",
852        ])?;
853
854        let expected = vec![
855            AppCommand::NoOp,
856            AppCommand::Kill { app: fenix() },
857            AppCommand::Reset { app: fenix() },
858            AppCommand::Enroll {
859                app: fenix(),
860                params: fenix_params(),
861                experiment: experiment("my-experiment"),
862                rollouts: Default::default(),
863                branch: "my-branch".to_string(),
864                preserve_targeting: false,
865                preserve_bucketing: false,
866                preserve_nimbus_db: false,
867                open: Default::default(),
868            },
869        ];
870        assert_eq!(expected, observed);
871        Ok(())
872    }
873
874    #[test]
875    fn test_enroll_with_validate() -> Result<()> {
876        let observed = get_commands_from_cli([
877            "nimbus-cli",
878            "--app",
879            "fenix",
880            "--channel",
881            "developer",
882            "enroll",
883            "my-experiment",
884            "--branch",
885            "my-branch",
886            "--reset-app",
887        ])?;
888
889        let expected = vec![
890            AppCommand::ValidateExperiment {
891                params: fenix_params(),
892                manifest: fenix_manifest(),
893                experiment: experiment("my-experiment"),
894            },
895            AppCommand::Kill { app: fenix() },
896            AppCommand::Reset { app: fenix() },
897            AppCommand::Enroll {
898                app: fenix(),
899                params: fenix_params(),
900                experiment: experiment("my-experiment"),
901                rollouts: Default::default(),
902                branch: "my-branch".to_string(),
903                preserve_targeting: false,
904                preserve_bucketing: false,
905                preserve_nimbus_db: false,
906                open: Default::default(),
907            },
908        ];
909        assert_eq!(expected, observed);
910        Ok(())
911    }
912
913    #[test]
914    fn test_enroll_with_deeplink() -> Result<()> {
915        let observed = get_commands_from_cli([
916            "nimbus-cli",
917            "--app",
918            "fenix",
919            "--channel",
920            "developer",
921            "enroll",
922            "my-experiment",
923            "--branch",
924            "my-branch",
925            "--no-validate",
926            "--deeplink",
927            "host/path?key=value",
928        ])?;
929
930        let expected = vec![
931            AppCommand::NoOp,
932            AppCommand::Kill { app: fenix() },
933            AppCommand::Enroll {
934                app: fenix(),
935                params: fenix_params(),
936                experiment: experiment("my-experiment"),
937                rollouts: Default::default(),
938                branch: "my-branch".to_string(),
939                preserve_targeting: false,
940                preserve_bucketing: false,
941                preserve_nimbus_db: false,
942                open: with_deeplink("host/path?key=value"),
943            },
944        ];
945        assert_eq!(expected, observed);
946        Ok(())
947    }
948
949    #[test]
950    fn test_enroll_with_passthrough() -> Result<()> {
951        let observed = get_commands_from_cli([
952            "nimbus-cli",
953            "--app",
954            "fenix",
955            "--channel",
956            "developer",
957            "enroll",
958            "my-experiment",
959            "--branch",
960            "my-branch",
961            "--no-validate",
962            "--",
963            "--start-profiler",
964            "./profile.file",
965            "{}",
966            "--esn",
967            "TEST_FLAG",
968        ])?;
969
970        let expected = vec![
971            AppCommand::NoOp,
972            AppCommand::Kill { app: fenix() },
973            AppCommand::Enroll {
974                app: fenix(),
975                params: fenix_params(),
976                experiment: experiment("my-experiment"),
977                rollouts: Default::default(),
978                branch: "my-branch".to_string(),
979                preserve_targeting: false,
980                preserve_bucketing: false,
981                preserve_nimbus_db: false,
982                open: with_passthrough(&[
983                    "--start-profiler",
984                    "./profile.file",
985                    "{}",
986                    "--esn",
987                    "TEST_FLAG",
988                ]),
989            },
990        ];
991        assert_eq!(expected, observed);
992        Ok(())
993    }
994
995    #[test]
996    fn test_enroll_with_pbcopy() -> Result<()> {
997        let observed = get_commands_from_cli([
998            "nimbus-cli",
999            "--app",
1000            "fenix",
1001            "--channel",
1002            "developer",
1003            "enroll",
1004            "my-experiment",
1005            "--branch",
1006            "my-branch",
1007            "--no-validate",
1008            "--pbcopy",
1009        ])?;
1010
1011        let expected = vec![
1012            AppCommand::NoOp,
1013            AppCommand::Enroll {
1014                app: fenix(),
1015                params: fenix_params(),
1016                experiment: experiment("my-experiment"),
1017                rollouts: Default::default(),
1018                branch: "my-branch".to_string(),
1019                preserve_targeting: false,
1020                preserve_bucketing: false,
1021                preserve_nimbus_db: false,
1022                open: with_pbcopy(),
1023            },
1024        ];
1025        assert_eq!(expected, observed);
1026        Ok(())
1027    }
1028
1029    #[test]
1030    fn test_enroll_with_output() -> Result<()> {
1031        let observed = get_commands_from_cli([
1032            "nimbus-cli",
1033            "--app",
1034            "fenix",
1035            "--channel",
1036            "developer",
1037            "enroll",
1038            "my-experiment",
1039            "--branch",
1040            "my-branch",
1041            "--no-validate",
1042            "--output",
1043            "./file.json",
1044        ])?;
1045
1046        let expected = vec![
1047            AppCommand::NoOp,
1048            AppCommand::Enroll {
1049                app: fenix(),
1050                params: fenix_params(),
1051                experiment: experiment("my-experiment"),
1052                rollouts: Default::default(),
1053                branch: "my-branch".to_string(),
1054                preserve_targeting: false,
1055                preserve_bucketing: false,
1056                preserve_nimbus_db: false,
1057                open: with_output("./file.json"),
1058            },
1059        ];
1060        assert_eq!(expected, observed);
1061        Ok(())
1062    }
1063
1064    #[test]
1065    fn test_validate() -> Result<()> {
1066        let observed = get_commands_from_cli([
1067            "nimbus-cli",
1068            "--app",
1069            "fenix",
1070            "--channel",
1071            "developer",
1072            "validate",
1073            "my-experiment",
1074        ])?;
1075
1076        let expected = vec![
1077            AppCommand::ValidateExperiment {
1078                params: fenix_params(),
1079                manifest: fenix_manifest(),
1080                experiment: experiment("my-experiment"),
1081            },
1082            AppCommand::NoOp,
1083        ];
1084        assert_eq!(expected, observed);
1085
1086        // With a specific version of the manifest.
1087        let observed = get_commands_from_cli([
1088            "nimbus-cli",
1089            "--app",
1090            "fenix",
1091            "--channel",
1092            "developer",
1093            "validate",
1094            "my-experiment",
1095            "--version",
1096            "114",
1097        ])?;
1098
1099        let expected = vec![
1100            AppCommand::ValidateExperiment {
1101                params: fenix_params(),
1102                manifest: fenix_old_manifest_with_ref("releases_v114"),
1103                experiment: experiment("my-experiment"),
1104            },
1105            AppCommand::NoOp,
1106        ];
1107        assert_eq!(expected, observed);
1108
1109        // With a specific version of the manifest, via a ref.
1110        let observed = get_commands_from_cli([
1111            "nimbus-cli",
1112            "--app",
1113            "fenix",
1114            "--channel",
1115            "developer",
1116            "validate",
1117            "my-experiment",
1118            "--ref",
1119            "my-tag",
1120        ])?;
1121
1122        let expected = vec![
1123            AppCommand::ValidateExperiment {
1124                params: fenix_params(),
1125                manifest: fenix_manifest_with_ref("my-tag"),
1126                experiment: experiment("my-experiment"),
1127            },
1128            AppCommand::NoOp,
1129        ];
1130        assert_eq!(expected, observed);
1131
1132        // With a file on disk
1133        let observed = get_commands_from_cli([
1134            "nimbus-cli",
1135            "--channel",
1136            "developer",
1137            "validate",
1138            "my-experiment",
1139            "--manifest",
1140            "./manifest.fml.yaml",
1141        ])?;
1142
1143        let expected = vec![
1144            AppCommand::ValidateExperiment {
1145                params: NimbusApp {
1146                    channel: Some("developer".to_string()),
1147                    app_name: None,
1148                },
1149                manifest: manifest_from_file("./manifest.fml.yaml"),
1150                experiment: experiment("my-experiment"),
1151            },
1152            AppCommand::NoOp,
1153        ];
1154        assert_eq!(expected, observed);
1155
1156        Ok(())
1157    }
1158
1159    #[test]
1160    fn test_test_feature() -> Result<()> {
1161        let observed = get_commands_from_cli([
1162            "nimbus-cli",
1163            "--app",
1164            "fenix",
1165            "--channel",
1166            "developer",
1167            "test-feature",
1168            "my-feature",
1169            "./my-branch.json",
1170            "./my-treatment.json",
1171        ])?;
1172
1173        let expected = vec![
1174            AppCommand::ValidateExperiment {
1175                params: fenix_params(),
1176                manifest: fenix_manifest(),
1177                experiment: feature_experiment(
1178                    "my-feature",
1179                    &["./my-branch.json", "./my-treatment.json"],
1180                ),
1181            },
1182            AppCommand::Kill { app: fenix() },
1183            AppCommand::Enroll {
1184                app: fenix(),
1185                params: fenix_params(),
1186                experiment: feature_experiment(
1187                    "my-feature",
1188                    &["./my-branch.json", "./my-treatment.json"],
1189                ),
1190                rollouts: Default::default(),
1191                branch: "my-branch".to_string(),
1192                preserve_targeting: false,
1193                preserve_bucketing: false,
1194                preserve_nimbus_db: false,
1195                open: Default::default(),
1196            },
1197        ];
1198        assert_eq!(expected, observed);
1199
1200        // With a specific version of the manifest.
1201        let observed = get_commands_from_cli([
1202            "nimbus-cli",
1203            "--app",
1204            "fenix",
1205            "--channel",
1206            "developer",
1207            "test-feature",
1208            "my-feature",
1209            "./my-branch.json",
1210            "./my-treatment.json",
1211            "--version",
1212            "114",
1213        ])?;
1214
1215        let expected = vec![
1216            AppCommand::ValidateExperiment {
1217                params: fenix_params(),
1218                manifest: fenix_old_manifest_with_ref("releases_v114"),
1219                experiment: feature_experiment(
1220                    "my-feature",
1221                    &["./my-branch.json", "./my-treatment.json"],
1222                ),
1223            },
1224            AppCommand::Kill { app: fenix() },
1225            AppCommand::Enroll {
1226                app: fenix(),
1227                params: fenix_params(),
1228                experiment: feature_experiment(
1229                    "my-feature",
1230                    &["./my-branch.json", "./my-treatment.json"],
1231                ),
1232                rollouts: Default::default(),
1233                branch: "my-branch".to_string(),
1234                preserve_targeting: false,
1235                preserve_bucketing: false,
1236                preserve_nimbus_db: false,
1237                open: Default::default(),
1238            },
1239        ];
1240        assert_eq!(expected, observed);
1241
1242        // With a specific version of the manifest, via a ref.
1243        let observed = get_commands_from_cli([
1244            "nimbus-cli",
1245            "--app",
1246            "fenix",
1247            "--channel",
1248            "developer",
1249            "test-feature",
1250            "my-feature",
1251            "./my-branch.json",
1252            "./my-treatment.json",
1253            "--ref",
1254            "my-tag",
1255        ])?;
1256
1257        let expected = vec![
1258            AppCommand::ValidateExperiment {
1259                params: fenix_params(),
1260                manifest: fenix_manifest_with_ref("my-tag"),
1261                experiment: feature_experiment(
1262                    "my-feature",
1263                    &["./my-branch.json", "./my-treatment.json"],
1264                ),
1265            },
1266            AppCommand::Kill { app: fenix() },
1267            AppCommand::Enroll {
1268                app: fenix(),
1269                params: fenix_params(),
1270                experiment: feature_experiment(
1271                    "my-feature",
1272                    &["./my-branch.json", "./my-treatment.json"],
1273                ),
1274                rollouts: Default::default(),
1275                branch: "my-branch".to_string(),
1276                preserve_targeting: false,
1277                preserve_bucketing: false,
1278                preserve_nimbus_db: false,
1279                open: Default::default(),
1280            },
1281        ];
1282        assert_eq!(expected, observed);
1283
1284        // With a file on disk
1285        let observed = get_commands_from_cli([
1286            "nimbus-cli",
1287            "--app",
1288            "fenix",
1289            "--channel",
1290            "developer",
1291            "test-feature",
1292            "my-feature",
1293            "./my-branch.json",
1294            "./my-treatment.json",
1295            "--manifest",
1296            "./manifest.fml.yaml",
1297        ])?;
1298
1299        let expected = vec![
1300            AppCommand::ValidateExperiment {
1301                params: fenix_params(),
1302                manifest: manifest_from_file("./manifest.fml.yaml"),
1303                experiment: feature_experiment(
1304                    "my-feature",
1305                    &["./my-branch.json", "./my-treatment.json"],
1306                ),
1307            },
1308            AppCommand::Kill { app: fenix() },
1309            AppCommand::Enroll {
1310                app: fenix(),
1311                params: fenix_params(),
1312                experiment: feature_experiment(
1313                    "my-feature",
1314                    &["./my-branch.json", "./my-treatment.json"],
1315                ),
1316                rollouts: Default::default(),
1317                branch: "my-branch".to_string(),
1318                preserve_targeting: false,
1319                preserve_bucketing: false,
1320                preserve_nimbus_db: false,
1321                open: Default::default(),
1322            },
1323        ];
1324        assert_eq!(expected, observed);
1325
1326        let observed = get_commands_from_cli([
1327            "nimbus-cli",
1328            "--app",
1329            "fenix",
1330            "--channel",
1331            "developer",
1332            "test-feature",
1333            "my-feature",
1334            "./my-branch.json",
1335            "./my-treatment.json",
1336            "--no-validate",
1337            "--deeplink",
1338            "host/path?key=value",
1339        ])?;
1340
1341        let expected = vec![
1342            AppCommand::NoOp,
1343            AppCommand::Kill { app: fenix() },
1344            AppCommand::Enroll {
1345                app: fenix(),
1346                params: fenix_params(),
1347                experiment: feature_experiment(
1348                    "my-feature",
1349                    &["./my-branch.json", "./my-treatment.json"],
1350                ),
1351                rollouts: Default::default(),
1352                branch: "my-branch".to_string(),
1353                preserve_targeting: false,
1354                preserve_bucketing: false,
1355                preserve_nimbus_db: false,
1356                open: with_deeplink("host/path?key=value"),
1357            },
1358        ];
1359        assert_eq!(expected, observed);
1360
1361        Ok(())
1362    }
1363
1364    #[test]
1365    fn test_open() -> Result<()> {
1366        let observed = get_commands_from_cli([
1367            "nimbus-cli",
1368            "--app",
1369            "fenix",
1370            "--channel",
1371            "developer",
1372            "open",
1373        ])?;
1374
1375        let expected = vec![
1376            AppCommand::NoOp,
1377            AppCommand::Kill { app: fenix() },
1378            AppCommand::Open {
1379                app: fenix(),
1380                open: Default::default(),
1381            },
1382        ];
1383        assert_eq!(expected, observed);
1384        Ok(())
1385    }
1386
1387    #[test]
1388    fn test_open_with_reset() -> Result<()> {
1389        let observed = get_commands_from_cli([
1390            "nimbus-cli",
1391            "--app",
1392            "fenix",
1393            "--channel",
1394            "developer",
1395            "open",
1396            "--reset-app",
1397        ])?;
1398
1399        let expected = vec![
1400            AppCommand::NoOp,
1401            AppCommand::Kill { app: fenix() },
1402            AppCommand::Reset { app: fenix() },
1403            AppCommand::Open {
1404                app: fenix(),
1405                open: Default::default(),
1406            },
1407        ];
1408        assert_eq!(expected, observed);
1409        Ok(())
1410    }
1411
1412    #[test]
1413    fn test_open_with_deeplink() -> Result<()> {
1414        let observed = get_commands_from_cli([
1415            "nimbus-cli",
1416            "--app",
1417            "fenix",
1418            "--channel",
1419            "developer",
1420            "open",
1421            "--deeplink",
1422            "host/path",
1423        ])?;
1424
1425        let expected = vec![
1426            AppCommand::NoOp,
1427            AppCommand::Kill { app: fenix() },
1428            AppCommand::Open {
1429                app: fenix(),
1430                open: with_deeplink("host/path"),
1431            },
1432        ];
1433        assert_eq!(expected, observed);
1434
1435        Ok(())
1436    }
1437
1438    #[test]
1439    fn test_open_with_passthrough_params() -> Result<()> {
1440        let observed = get_commands_from_cli([
1441            "nimbus-cli",
1442            "--app",
1443            "fenix",
1444            "--channel",
1445            "developer",
1446            "open",
1447            "--",
1448            "--start-profiler",
1449            "./profile.file",
1450            "{}",
1451            "--esn",
1452            "TEST_FLAG",
1453        ])?;
1454
1455        let expected = vec![
1456            AppCommand::NoOp,
1457            AppCommand::Kill { app: fenix() },
1458            AppCommand::Open {
1459                app: fenix(),
1460                open: with_passthrough(&[
1461                    "--start-profiler",
1462                    "./profile.file",
1463                    "{}",
1464                    "--esn",
1465                    "TEST_FLAG",
1466                ]),
1467            },
1468        ];
1469        assert_eq!(expected, observed);
1470        Ok(())
1471    }
1472
1473    #[test]
1474    fn test_open_with_noclobber() -> Result<()> {
1475        let observed = get_commands_from_cli([
1476            "nimbus-cli",
1477            "--app",
1478            "fenix",
1479            "--channel",
1480            "developer",
1481            "open",
1482            "--no-clobber",
1483        ])?;
1484
1485        let expected = vec![
1486            AppCommand::NoOp,
1487            AppCommand::Open {
1488                app: fenix(),
1489                open: Default::default(),
1490            },
1491        ];
1492        assert_eq!(expected, observed);
1493        Ok(())
1494    }
1495
1496    #[test]
1497    fn test_open_with_pbcopy() -> Result<()> {
1498        let observed = get_commands_from_cli([
1499            "nimbus-cli",
1500            "--app",
1501            "fenix",
1502            "--channel",
1503            "developer",
1504            "open",
1505            "--pbcopy",
1506        ])?;
1507
1508        let expected = vec![
1509            AppCommand::NoOp,
1510            AppCommand::Open {
1511                app: fenix(),
1512                open: with_pbcopy(),
1513            },
1514        ];
1515        assert_eq!(expected, observed);
1516        Ok(())
1517    }
1518
1519    #[test]
1520    fn test_fetch() -> Result<()> {
1521        let file = Some(PathBuf::from("./archived.json"));
1522        let observed = get_commands_from_cli([
1523            "nimbus-cli",
1524            "--app",
1525            "fenix",
1526            "--channel",
1527            "developer",
1528            "fetch",
1529            "--output",
1530            "./archived.json",
1531            "my-experiment",
1532        ])?;
1533
1534        let expected = vec![
1535            AppCommand::NoOp,
1536            AppCommand::FetchList {
1537                list: for_app(
1538                    "fenix",
1539                    ExperimentListSource::FromRecipes {
1540                        recipes: vec![experiment("my-experiment")],
1541                    },
1542                ),
1543                file: file.clone(),
1544            },
1545        ];
1546        assert_eq!(expected, observed);
1547
1548        let observed = get_commands_from_cli([
1549            "nimbus-cli",
1550            "--app",
1551            "fenix",
1552            "--channel",
1553            "developer",
1554            "fetch",
1555            "--output",
1556            "./archived.json",
1557            "my-experiment-1",
1558            "my-experiment-2",
1559        ])?;
1560
1561        let expected = vec![
1562            AppCommand::NoOp,
1563            AppCommand::FetchList {
1564                list: for_app(
1565                    "fenix",
1566                    ExperimentListSource::FromRecipes {
1567                        recipes: vec![experiment("my-experiment-1"), experiment("my-experiment-2")],
1568                    },
1569                ),
1570                file,
1571            },
1572        ];
1573        assert_eq!(expected, observed);
1574        Ok(())
1575    }
1576
1577    #[test]
1578    fn test_fetch_list() -> Result<()> {
1579        let file = Some(PathBuf::from("./archived.json"));
1580        let observed =
1581            get_commands_from_cli(["nimbus-cli", "fetch-list", "--output", "./archived.json"])?;
1582
1583        let expected = vec![
1584            AppCommand::NoOp,
1585            AppCommand::FetchList {
1586                list: ExperimentListSource::FromRemoteSettings {
1587                    endpoint: config::rs_production_server(),
1588                    is_preview: false,
1589                },
1590                file: file.clone(),
1591            },
1592        ];
1593        assert_eq!(expected, observed);
1594
1595        let observed = get_commands_from_cli([
1596            "nimbus-cli",
1597            "--app",
1598            "fenix",
1599            "fetch-list",
1600            "--output",
1601            "./archived.json",
1602        ])?;
1603
1604        let expected = vec![
1605            AppCommand::NoOp,
1606            AppCommand::FetchList {
1607                list: for_app(
1608                    "fenix",
1609                    ExperimentListSource::FromRemoteSettings {
1610                        endpoint: config::rs_production_server(),
1611                        is_preview: false,
1612                    },
1613                ),
1614                file: file.clone(),
1615            },
1616        ];
1617        assert_eq!(expected, observed);
1618
1619        let observed = get_commands_from_cli([
1620            "nimbus-cli",
1621            "fetch-list",
1622            "--output",
1623            "./archived.json",
1624            "stage",
1625        ])?;
1626
1627        let expected = vec![
1628            AppCommand::NoOp,
1629            AppCommand::FetchList {
1630                list: ExperimentListSource::FromRemoteSettings {
1631                    endpoint: config::rs_stage_server(),
1632                    is_preview: false,
1633                },
1634                file: file.clone(),
1635            },
1636        ];
1637        assert_eq!(expected, observed);
1638
1639        let observed = get_commands_from_cli([
1640            "nimbus-cli",
1641            "fetch-list",
1642            "--output",
1643            "./archived.json",
1644            "preview",
1645        ])?;
1646
1647        let expected = vec![
1648            AppCommand::NoOp,
1649            AppCommand::FetchList {
1650                list: ExperimentListSource::FromRemoteSettings {
1651                    endpoint: config::rs_production_server(),
1652                    is_preview: true,
1653                },
1654                file: file.clone(),
1655            },
1656        ];
1657        assert_eq!(expected, observed);
1658
1659        let observed = get_commands_from_cli([
1660            "nimbus-cli",
1661            "fetch-list",
1662            "--output",
1663            "./archived.json",
1664            "--use-api",
1665        ])?;
1666
1667        let expected = vec![
1668            AppCommand::NoOp,
1669            AppCommand::FetchList {
1670                list: ExperimentListSource::FromApiV6 {
1671                    endpoint: config::api_v6_production_server(),
1672                },
1673                file: file.clone(),
1674            },
1675        ];
1676        assert_eq!(expected, observed);
1677
1678        let observed = get_commands_from_cli([
1679            "nimbus-cli",
1680            "fetch-list",
1681            "--use-api",
1682            "--output",
1683            "./archived.json",
1684            "stage",
1685        ])?;
1686
1687        let expected = vec![
1688            AppCommand::NoOp,
1689            AppCommand::FetchList {
1690                list: ExperimentListSource::FromApiV6 {
1691                    endpoint: config::api_v6_stage_server(),
1692                },
1693                file,
1694            },
1695        ];
1696        assert_eq!(expected, observed);
1697
1698        Ok(())
1699    }
1700
1701    #[test]
1702    fn test_list() -> Result<()> {
1703        let observed = get_commands_from_cli(["nimbus-cli", "list"])?;
1704        let expected = vec![
1705            AppCommand::NoOp,
1706            AppCommand::List {
1707                list: ExperimentListSource::FromRemoteSettings {
1708                    endpoint: config::rs_production_server(),
1709                    is_preview: false,
1710                },
1711            },
1712        ];
1713        assert_eq!(expected, observed);
1714
1715        let observed = get_commands_from_cli(["nimbus-cli", "list", "preview"])?;
1716        let expected = vec![
1717            AppCommand::NoOp,
1718            AppCommand::List {
1719                list: ExperimentListSource::FromRemoteSettings {
1720                    endpoint: config::rs_production_server(),
1721                    is_preview: true,
1722                },
1723            },
1724        ];
1725        assert_eq!(expected, observed);
1726
1727        let observed = get_commands_from_cli(["nimbus-cli", "list", "stage"])?;
1728        let expected = vec![
1729            AppCommand::NoOp,
1730            AppCommand::List {
1731                list: ExperimentListSource::FromRemoteSettings {
1732                    endpoint: config::rs_stage_server(),
1733                    is_preview: false,
1734                },
1735            },
1736        ];
1737        assert_eq!(expected, observed);
1738
1739        let observed = get_commands_from_cli(["nimbus-cli", "list", "--use-api", "stage"])?;
1740        let expected = vec![
1741            AppCommand::NoOp,
1742            AppCommand::List {
1743                list: ExperimentListSource::FromApiV6 {
1744                    endpoint: config::api_v6_stage_server(),
1745                },
1746            },
1747        ];
1748        assert_eq!(expected, observed);
1749
1750        let observed = get_commands_from_cli(["nimbus-cli", "list", "--use-api"])?;
1751        let expected = vec![
1752            AppCommand::NoOp,
1753            AppCommand::List {
1754                list: ExperimentListSource::FromApiV6 {
1755                    endpoint: config::api_v6_production_server(),
1756                },
1757            },
1758        ];
1759        assert_eq!(expected, observed);
1760        Ok(())
1761    }
1762
1763    #[test]
1764    fn test_list_filter() -> Result<()> {
1765        let observed = get_commands_from_cli(["nimbus-cli", "--app", "my-app", "list"])?;
1766        let expected = vec![
1767            AppCommand::NoOp,
1768            AppCommand::List {
1769                list: for_app(
1770                    "my-app",
1771                    ExperimentListSource::FromRemoteSettings {
1772                        endpoint: config::rs_production_server(),
1773                        is_preview: false,
1774                    },
1775                ),
1776            },
1777        ];
1778        assert_eq!(expected, observed);
1779
1780        let observed = get_commands_from_cli(["nimbus-cli", "list", "--feature", "messaging"])?;
1781        let expected = vec![
1782            AppCommand::NoOp,
1783            AppCommand::List {
1784                list: for_feature(
1785                    "messaging",
1786                    ExperimentListSource::FromRemoteSettings {
1787                        endpoint: config::rs_production_server(),
1788                        is_preview: false,
1789                    },
1790                ),
1791            },
1792        ];
1793        assert_eq!(expected, observed);
1794
1795        let observed = get_commands_from_cli([
1796            "nimbus-cli",
1797            "--app",
1798            "my-app",
1799            "list",
1800            "--feature",
1801            "messaging",
1802        ])?;
1803        let expected = vec![
1804            AppCommand::NoOp,
1805            AppCommand::List {
1806                list: for_app(
1807                    "my-app",
1808                    for_feature(
1809                        "messaging",
1810                        ExperimentListSource::FromRemoteSettings {
1811                            endpoint: config::rs_production_server(),
1812                            is_preview: false,
1813                        },
1814                    ),
1815                ),
1816            },
1817        ];
1818        assert_eq!(expected, observed);
1819
1820        Ok(())
1821    }
1822
1823    #[test]
1824    fn test_list_filter_by_date_with_error() -> Result<()> {
1825        let observed = get_commands_from_cli(["nimbus-cli", "list", "--active-on", "FOO"]);
1826        assert!(observed.is_err());
1827        let err = observed.unwrap_err();
1828        assert!(err.to_string().contains("Date string must be yyyy-mm-dd"));
1829
1830        let observed = get_commands_from_cli(["nimbus-cli", "list", "--enrolling-on", "FOO"]);
1831        assert!(observed.is_err());
1832        let err = observed.unwrap_err();
1833        assert!(err.to_string().contains("Date string must be yyyy-mm-dd"));
1834        Ok(())
1835    }
1836
1837    #[test]
1838    fn test_list_filter_by_dates() -> Result<()> {
1839        let today = "1970-01-01";
1840
1841        let observed = get_commands_from_cli(["nimbus-cli", "list", "--active-on", today])?;
1842        let expected = vec![
1843            AppCommand::NoOp,
1844            AppCommand::List {
1845                list: for_active_on_date(
1846                    today,
1847                    ExperimentListSource::FromRemoteSettings {
1848                        endpoint: config::rs_production_server(),
1849                        is_preview: false,
1850                    },
1851                ),
1852            },
1853        ];
1854        assert_eq!(expected, observed);
1855
1856        let observed = get_commands_from_cli(["nimbus-cli", "list", "--enrolling-on", today])?;
1857        let expected = vec![
1858            AppCommand::NoOp,
1859            AppCommand::List {
1860                list: for_enrolling_on_date(
1861                    today,
1862                    ExperimentListSource::FromRemoteSettings {
1863                        endpoint: config::rs_production_server(),
1864                        is_preview: false,
1865                    },
1866                ),
1867            },
1868        ];
1869        assert_eq!(expected, observed);
1870
1871        Ok(())
1872    }
1873}