aboutsummaryrefslogtreecommitdiff
path: root/src/tests
diff options
context:
space:
mode:
authorDaniel Mueller <deso@posteo.net>2020-08-25 19:04:50 -0700
committerDaniel Mueller <deso@posteo.net>2020-10-11 17:07:07 -0700
commit8cf63c6790192c30c81294e7a940d470bf061cbf (patch)
tree169ca792304f74a7b1548b5bad798b39649cd4c5 /src/tests
parent330142e68cac0116babbf2fd64fc9ff0fde132c0 (diff)
downloadnitrocli-8cf63c6790192c30c81294e7a940d470bf061cbf.tar.gz
nitrocli-8cf63c6790192c30c81294e7a940d470bf061cbf.tar.bz2
Introduce support for user-provided extensions
This change introduces support for discovering and executing user-provided extensions to the program. Extensions are useful for allowing users to provide additional functionality on top of the nitrocli proper. Implementation wise we stick to an approach similar to git or cargo subcommands in nature: we search the directories listed in the PATH environment variable for a file that starts with "nitrocli-", followed by the extension name. This file is then executed. It is assumed that the extension recognizes (or at least not prohibits) the following arguments: --nitrocli (providing the path to the nitrocli binary), --model (with the model passed to the main program), and --verbosity (the verbosity level).
Diffstat (limited to 'src/tests')
-rw-r--r--src/tests/extension_var_test.py61
-rw-r--r--src/tests/extensions.rs43
-rw-r--r--src/tests/mod.rs10
-rw-r--r--src/tests/run.rs115
4 files changed, 229 insertions, 0 deletions
diff --git a/src/tests/extension_var_test.py b/src/tests/extension_var_test.py
new file mode 100644
index 0000000..af7ec84
--- /dev/null
+++ b/src/tests/extension_var_test.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2020 The Nitrocli Developers
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from argparse import (
+ ArgumentParser,
+)
+from enum import (
+ Enum,
+)
+from os import (
+ environ,
+)
+from sys import (
+ argv,
+ exit,
+)
+
+
+class Action(Enum):
+ """An action to perform."""
+ BINARY = "binary"
+ MODEL = "model"
+ NO_CACHE = "no-cache"
+ SERIAL_NUMBERS = "serial-numbers"
+ USB_PATH = "usb-path"
+ VERBOSITY = "verbosity"
+
+ @classmethod
+ def all(cls):
+ """Return the list of all the enum members' values."""
+ return [x.value for x in cls.__members__.values()]
+
+
+def main(args):
+ """The extension's main function."""
+ parser = ArgumentParser()
+ parser.add_argument(choices=Action.all(), dest="what")
+ parser.add_argument("--nitrocli", action="store", default=None)
+ parser.add_argument("--model", action="store", default=None)
+ # We deliberately store the argument to this option as a string
+ # because we can differentiate between None and a valid value, in
+ # order to verify that it really is supplied.
+ parser.add_argument("--verbosity", action="store", default=None)
+
+ namespace = parser.parse_args(args[1:])
+ # We create a "reverse" mapping from string to variant (e.g., model ->
+ # MODEL).
+ options = {v.value: k for k, v in Action.__members__.items()}
+ try:
+ var = options[namespace.what]
+ print(environ[f"NITROCLI_{var}"])
+ except KeyError:
+ return 1
+
+ return 0
+
+
+if __name__ == "__main__":
+ exit(main(argv))
diff --git a/src/tests/extensions.rs b/src/tests/extensions.rs
new file mode 100644
index 0000000..a295949
--- /dev/null
+++ b/src/tests/extensions.rs
@@ -0,0 +1,43 @@
+// extensions.rs
+
+// Copyright (C) 2020 The Nitrocli Developers
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+use std::env;
+use std::fs;
+
+use super::*;
+
+#[test]
+fn resolve_extensions() -> anyhow::Result<()> {
+ let dir1 = tempfile::tempdir()?;
+ let dir2 = tempfile::tempdir()?;
+
+ {
+ let ext1_path = dir1.path().join("nitrocli-ext1");
+ let ext2_path = dir1.path().join("nitrocli-ext2");
+ let ext3_path = dir2.path().join("nitrocli-super-1337-extensions111one");
+ let _ext1 = fs::File::create(&ext1_path)?;
+ let _ext2 = fs::File::create(&ext2_path)?;
+ let _ext3 = fs::File::create(&ext3_path)?;
+
+ let path = env::join_paths(&[dir1.path(), dir2.path()])?;
+ assert_eq!(
+ crate::commands::resolve_extension(&path, ffi::OsStr::new("ext1"))?,
+ ext1_path
+ );
+ assert_eq!(
+ crate::commands::resolve_extension(&path, ffi::OsStr::new("ext2"))?,
+ ext2_path
+ );
+ assert_eq!(
+ crate::commands::resolve_extension(&path, ffi::OsStr::new("super-1337-extensions111one"))?,
+ ext3_path
+ );
+
+ let err = crate::commands::resolve_extension(&ffi::OsStr::new(""), ffi::OsStr::new("ext1"))
+ .unwrap_err();
+ assert_eq!(err.to_string(), "Extension nitrocli-ext1 not found");
+ }
+ Ok(())
+}
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index 65983bb..5e47520 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -9,6 +9,7 @@ use nitrokey_test::test as test_device;
mod config;
mod encrypted;
+mod extensions;
mod fill;
mod hidden;
mod list;
@@ -23,6 +24,7 @@ mod unencrypted;
struct Nitrocli {
model: Option<nitrokey::Model>,
+ path: Option<ffi::OsString>,
admin_pin: Option<ffi::OsString>,
user_pin: Option<ffi::OsString>,
new_admin_pin: Option<ffi::OsString>,
@@ -34,6 +36,7 @@ impl Nitrocli {
pub fn new() -> Self {
Self {
model: None,
+ path: None,
admin_pin: Some(nitrokey::DEFAULT_ADMIN_PIN.into()),
user_pin: Some(nitrokey::DEFAULT_USER_PIN.into()),
new_admin_pin: None,
@@ -54,6 +57,12 @@ impl Nitrocli {
self
}
+ /// Set the `PATH` used for looking up extensions.
+ fn path(mut self, path: impl Into<ffi::OsString>) -> Self {
+ self.path = Some(path.into());
+ self
+ }
+
pub fn admin_pin(mut self, pin: impl Into<ffi::OsString>) -> Self {
self.admin_pin = Some(pin.into());
self
@@ -102,6 +111,7 @@ impl Nitrocli {
stdout: &mut stdout,
stderr: &mut stderr,
is_tty: false,
+ path: self.path.clone(),
admin_pin: self.admin_pin.clone(),
user_pin: self.user_pin.clone(),
new_admin_pin: self.new_admin_pin.clone(),
diff --git a/src/tests/run.rs b/src/tests/run.rs
index 33191d3..e4bbb28 100644
--- a/src/tests/run.rs
+++ b/src/tests/run.rs
@@ -5,8 +5,12 @@
use std::collections;
use std::convert;
+use std::convert::TryFrom as _;
use std::convert::TryInto as _;
+use std::fs;
+use std::io::Write;
use std::ops;
+use std::os::unix::fs::OpenOptionsExt;
use std::path;
use super::*;
@@ -277,3 +281,114 @@ fn connect_usb_path_model_wrong_serial(_model: nitrokey::Model) -> anyhow::Resul
}
Ok(())
}
+
+#[test]
+fn extension() -> anyhow::Result<()> {
+ let ext_dir = tempfile::tempdir()?;
+ {
+ let mut ext = fs::OpenOptions::new()
+ .create(true)
+ .mode(0o755)
+ .write(true)
+ .open(ext_dir.path().join("nitrocli-ext"))?;
+
+ ext.write_all(
+ br#"#!/usr/bin/env python
+print("success")
+"#,
+ )?;
+ }
+
+ let path = ext_dir.path().as_os_str().to_os_string();
+ let out = Nitrocli::new().path(path).handle(&["ext"])?;
+ assert_eq!(out, "success\n");
+ Ok(())
+}
+
+#[test]
+fn extension_failure() -> anyhow::Result<()> {
+ let ext_dir = tempfile::tempdir()?;
+ {
+ let mut ext = fs::OpenOptions::new()
+ .create(true)
+ .mode(0o755)
+ .write(true)
+ .open(ext_dir.path().join("nitrocli-ext"))?;
+
+ ext.write_all(
+ br#"#!/usr/bin/env python
+import sys
+sys.exit(42);
+"#,
+ )?;
+ }
+
+ let path = ext_dir.path().as_os_str().to_os_string();
+ let mut ncli = Nitrocli::new().path(path);
+
+ let err = ncli.handle(&["ext"]).unwrap_err();
+ // The extension is responsible for printing any error messages.
+ // Nitrocli is expected not to mess with them, including adding
+ // additional information.
+ if let Some(crate::DirectExitError(rc)) = err.downcast_ref::<crate::DirectExitError>() {
+ assert_eq!(*rc, 42)
+ } else {
+ panic!("encountered unexpected error: {:#}", err)
+ }
+
+ let (rc, out, err) = ncli.run(&["ext"]);
+ assert_eq!(rc, 42);
+ assert_eq!(out, b"");
+ assert_eq!(err, b"");
+ Ok(())
+}
+
+#[test_device]
+fn extension_arguments(model: nitrokey::Model) -> anyhow::Result<()> {
+ fn test<F>(model: nitrokey::Model, what: &str, args: &[&str], check: F) -> anyhow::Result<()>
+ where
+ F: FnOnce(&str) -> bool,
+ {
+ let ext_dir = tempfile::tempdir()?;
+ {
+ let mut ext = fs::OpenOptions::new()
+ .create(true)
+ .mode(0o755)
+ .write(true)
+ .open(ext_dir.path().join("nitrocli-ext"))?;
+
+ ext.write_all(include_bytes!("extension_var_test.py"))?;
+ }
+
+ let mut args = args.to_vec();
+ args.append(&mut vec!["ext", what]);
+
+ let path = ext_dir.path().as_os_str().to_os_string();
+ let out = Nitrocli::new().model(model).path(path).handle(&args)?;
+
+ assert!(check(&out), out);
+ Ok(())
+ }
+
+ test(model, "binary", &[], |out| {
+ path::Path::new(out)
+ .file_stem()
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .trim()
+ .contains("nitrocli")
+ })?;
+ test(model, "model", &[], |out| {
+ out == args::DeviceModel::try_from(model).unwrap().to_string() + "\n"
+ })?;
+ test(model, "no-cache", &[], |out| out == "true\n")?;
+ test(model, "serial-numbers", &[], |out| out == "\n")?;
+ test(model, "verbosity", &[], |out| out == "0\n")?;
+ test(model, "verbosity", &["-v"], |out| out == "1\n")?;
+ test(model, "verbosity", &["-v", "--verbose"], |out| out == "2\n")?;
+
+ // NITROCLI_USB_PATH should not be set, so the program errors out.
+ let _ = test(model, "usb-path", &[], |out| out == "\n").unwrap_err();
+ Ok(())
+}