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