add integration/acceptance test with Subplot
These need to be run with the virtual smartcard emulation in the Docker container specified in .gitlab-ci.yml for tests. The tests are a little simplistic, as it turned out that making changes to the smart card results in flaky tests. Thus only parts of opgpcard that don't change the card are tested. Sponsored-by: NLnet Foundation; NGI Assure
This commit is contained in:
parent
0b616e7b6e
commit
dd02a29497
9 changed files with 327 additions and 3 deletions
27
tools/build.rs
Normal file
27
tools/build.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fir>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
// Only generate test code from the subplot, if a virtual smart
|
||||
// card is available. This is a kludge until Subplot can do
|
||||
// conditional scenarios
|
||||
// (https://gitlab.com/subplot/subplot/-/issues/20).
|
||||
let flagfile = Path::new("virtual-card-available");
|
||||
if flagfile.exists() {
|
||||
subplot_build::codegen("subplot/opgpcard.subplot")
|
||||
.expect("failed to generate code with Subplot");
|
||||
} else {
|
||||
// If we're not generating code from the subplot, we should at
|
||||
// least create an empty file so that the tests/opgpcard.rs
|
||||
// file can include it. Otherwise the build will fail.
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
let include = Path::new(&out_dir).join("opgpcard.rs");
|
||||
eprintln!("build.rs: include={}", include.display());
|
||||
if !include.exists() {
|
||||
File::create(include).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
23
tools/cargo-test-in-docker
Executable file
23
tools/cargo-test-in-docker
Executable file
|
@ -0,0 +1,23 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Run this to run opgpcard (tools directory) test suite inside a
|
||||
# Docker container with a virtual smartcard running. The test suite
|
||||
# contains tests that run the opgpcard binary and rely on a virtual
|
||||
# smart card to be available.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
docker run --rm -it \
|
||||
-v root:/root \
|
||||
-v cargo:/cargo \
|
||||
-v $(pwd):/src \
|
||||
registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps sh -c '
|
||||
sed -i "s/timeout=20/timeout=60/" /home/jcardsim/run-card.sh &&
|
||||
/etc/init.d/pcscd start &&
|
||||
su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim &&
|
||||
cd /src/tools &&
|
||||
if ! [ -e virtual-card-available ]; then rm -f tests/opgpcard.rs; fi &&
|
||||
CARGO_TARGET_DIR=/cargo/ cargo test'
|
|
@ -6,9 +6,8 @@ use std::path::PathBuf;
|
|||
|
||||
use crate::{OutputFormat, OutputVersion};
|
||||
|
||||
pub const DEFAULT_OUTPUT_VERSION: &str = "1.0.0";
|
||||
pub const OUTPUT_VERSIONS: &[OutputVersion] =
|
||||
&[OutputVersion::new(0, 0, 0), OutputVersion::new(1, 0, 0)];
|
||||
pub const DEFAULT_OUTPUT_VERSION: &str = "0.9.0";
|
||||
pub const OUTPUT_VERSIONS: &[OutputVersion] = &[OutputVersion::new(0, 9, 0)];
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
|
|
114
tools/subplot/opgpcard.md
Normal file
114
tools/subplot/opgpcard.md
Normal file
|
@ -0,0 +1,114 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
# Introduction
|
||||
|
||||
This document describes the requirements and acceptance criteria for
|
||||
the `opgpcard` tool, and also how to verify that they are met. This
|
||||
document is meant to be read and understood by all stakeholders, and
|
||||
processed by the [Subplot](https://subplot.tech/) tool, which also
|
||||
generates code to automatically perform the verification.
|
||||
|
||||
## Note about running the tests described here
|
||||
|
||||
The verification scenarios in this document assume the availability of
|
||||
a virtual smart card. Specifically one described in
|
||||
<https://gitlab.com/openpgp-card/virtual-cards>. The
|
||||
`openpgp-card/tools` crate is set up to generate tests only if the
|
||||
file `tools/virtual-card-available` exists, and the `openpgp-card`
|
||||
repository `.gitlab-ci.yml` file is set up to create that file when
|
||||
the repository is tested in GitLab CI.
|
||||
|
||||
This means that if you run `cargo test`, no test code is normally
|
||||
generated from this document. To run the tests locally, outside of
|
||||
GitLab CI, use the script `tools/cargo-test-in-docker`.
|
||||
|
||||
# Acceptance criteria
|
||||
|
||||
These scenarios mainly test the JSON output format of the tool. That
|
||||
format is meant for consumption by other tools, and it is thus more
|
||||
important that it stays stable. The text output that is meant for
|
||||
human consumption may change at will, so it's not worth testing.
|
||||
|
||||
## Smoke test
|
||||
|
||||
_Requirement: The tool can report its version._
|
||||
|
||||
Justification: This is useful mainly to make sure the tool can be run
|
||||
at all. As such, it acts as a simple [smoke
|
||||
test](https://en.wikipedia.org/wiki/Smoke_testing_(software)).
|
||||
However, if this fails, then nothing else has a chance to work.
|
||||
|
||||
Note that this is not in JSON format, as it is output by the `clap`
|
||||
library, and `opgpcard` doesn't affect what it looks like.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
when I run opgpcard --version
|
||||
then stdout matches regex ^opgpcard \d+\.\d+\.\d+$
|
||||
~~~
|
||||
|
||||
## List cards: `opgpcard list`
|
||||
|
||||
_Requirement: The tool lists available cards._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
when I run opgpcard --output-format=json list
|
||||
then stdout, as JSON, matches embedded file list.json
|
||||
~~~
|
||||
|
||||
~~~{#list.json .file .json}
|
||||
{
|
||||
"idents": ["AFAF:00001234"]
|
||||
}
|
||||
~~~
|
||||
|
||||
## Card status: `opgpcard status`
|
||||
|
||||
_Requirement: The tool shows status of available cards._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
when I run opgpcard --output-format=json status
|
||||
then stdout, as JSON, matches embedded file status.json
|
||||
~~~
|
||||
|
||||
~~~{#status.json .file .json}
|
||||
{
|
||||
"card_version": "2.0",
|
||||
"ident": "AFAF:00001234"
|
||||
}
|
||||
~~~
|
||||
|
||||
## Card information: `opgpcard info`
|
||||
|
||||
_Requirement: The tool shows information about available cards._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
when I run opgpcard --output-format=json info
|
||||
then stdout, as JSON, matches embedded file info.json
|
||||
~~~
|
||||
|
||||
~~~{#info.json .file .json}
|
||||
{
|
||||
"card_version": "2.0",
|
||||
"application_id": "D276000124 01 01 0200 AFAF 00001234 0000",
|
||||
"manufacturer_id": "AFAF",
|
||||
"manufacturer_name": "Unknown",
|
||||
"card_service_data": "",
|
||||
"ident": "AFAF:00001234"
|
||||
}
|
||||
~~~
|
99
tools/subplot/opgpcard.rs
Normal file
99
tools/subplot/opgpcard.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use subplotlib::file::SubplotDataFile;
|
||||
use subplotlib::steplibrary::runcmd::Runcmd;
|
||||
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SubplotContext {}
|
||||
|
||||
impl ContextElement for SubplotContext {}
|
||||
|
||||
#[step]
|
||||
#[context(SubplotContext)]
|
||||
#[context(Runcmd)]
|
||||
fn install_opgpcard(context: &ScenarioContext) {
|
||||
let target_exe = env!("CARGO_BIN_EXE_opgpcard");
|
||||
let target_path = Path::new(target_exe);
|
||||
let target_path = target_path.parent().ok_or("No parent?")?;
|
||||
context.with_mut(
|
||||
|context: &mut Runcmd| {
|
||||
context.prepend_to_path(target_path);
|
||||
Ok(())
|
||||
},
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
|
||||
#[step]
|
||||
#[context(Runcmd)]
|
||||
fn stdout_matches_json_file(context: &ScenarioContext, file: SubplotDataFile) {
|
||||
let expected: Value = serde_json::from_slice(file.data())?;
|
||||
println!("expecting JSON: {:#?}", expected);
|
||||
|
||||
let stdout = context.with(|runcmd: &Runcmd| Ok(runcmd.stdout_as_string()), false)?;
|
||||
let actual: Value = serde_json::from_str(&stdout)?;
|
||||
println!("stdout JSON: {:#?}", actual);
|
||||
|
||||
println!("fuzzy checking JSON values");
|
||||
assert!(json_eq(&actual, &expected));
|
||||
}
|
||||
|
||||
// Fuzzy match JSON values. For objects, anything in expected must be
|
||||
// in actual, but it's OK for there to be extra things.
|
||||
fn json_eq(actual: &Value, expected: &Value) -> bool {
|
||||
match actual {
|
||||
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
|
||||
println!("simple value");
|
||||
println!("actual ={:?}", actual);
|
||||
println!("expected={:?}", expected);
|
||||
let eq = actual == expected;
|
||||
println!("simple value eq={}", eq);
|
||||
return eq;
|
||||
}
|
||||
Value::Array(a_values) => {
|
||||
if let Value::Array(e_values) = expected {
|
||||
println!("both actual and equal are arrays");
|
||||
for (a_value, e_value) in a_values.iter().zip(e_values.iter()) {
|
||||
println!("comparing corresponding array elements");
|
||||
if !json_eq(a_value, e_value) {
|
||||
println!("array elements differ");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
println!("arrays match");
|
||||
return true;
|
||||
} else {
|
||||
println!("actual is array, expected is not");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Value::Object(a_obj) => {
|
||||
if let Value::Object(e_obj) = expected {
|
||||
println!("both actual and equal are objects");
|
||||
for key in e_obj.keys() {
|
||||
println!("checking key {}", key);
|
||||
if !a_obj.contains_key(key) {
|
||||
println!("key {} is missing from actual", key);
|
||||
return false;
|
||||
}
|
||||
let a_value = a_obj.get(key).unwrap();
|
||||
let e_value = e_obj.get(key).unwrap();
|
||||
let eq = json_eq(a_value, e_value);
|
||||
println!("values for {} eq={}", key, eq);
|
||||
if !eq {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
println!("objects match");
|
||||
return true;
|
||||
} else {
|
||||
println!("actual is object, expected is not");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
tools/subplot/opgpcard.subplot
Normal file
15
tools/subplot/opgpcard.subplot
Normal file
|
@ -0,0 +1,15 @@
|
|||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
title: "opgpcard acceptance tests"
|
||||
markdowns:
|
||||
- opgpcard.md
|
||||
bindings:
|
||||
- opgpcard.yaml
|
||||
- lib/files.yaml
|
||||
- lib/runcmd.yaml
|
||||
impls:
|
||||
rust:
|
||||
- opgpcard.rs
|
||||
classes:
|
||||
- json
|
12
tools/subplot/opgpcard.yaml
Normal file
12
tools/subplot/opgpcard.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
- given: an installed opgpcard
|
||||
impl:
|
||||
rust:
|
||||
function: install_opgpcard
|
||||
|
||||
- then: "stdout, as JSON, matches embedded file {file:file}"
|
||||
impl:
|
||||
rust:
|
||||
function: stdout_matches_json_file
|
29
tools/subplot/test-in-docker.sh
Executable file
29
tools/subplot/test-in-docker.sh
Executable file
|
@ -0,0 +1,29 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Run "cargo test" inside a Docker container with virtual cards
|
||||
# installed and running. This will allow at least rudimentary testing
|
||||
# of actual card functionality of opgpcard.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
image="registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps"
|
||||
|
||||
src="$(cd .. && pwd)"
|
||||
|
||||
if ! [ -e Cargo.toml ] || ! grep '^name.*openpgp-card-tools' Cargo.toml; then
|
||||
echo "MUST run this in the root of the openpgp-card-tool crate" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo build
|
||||
docker run --rm -t \
|
||||
--volume "cargo:/cargo" \
|
||||
--volume "dotcargo:/root/.cargo" \
|
||||
--volume "$src:/opt" "$image" \
|
||||
bash -c '
|
||||
/etc/init.d/pcscd start && \
|
||||
su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim && \
|
||||
cd /opt/tools && env CARGO_TARGET_DIR=/cargo cargo test'
|
6
tools/tests/opgpcard.rs
Normal file
6
tools/tests/opgpcard.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
#![allow(clippy::needless_return)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/opgpcard.rs"));
|
Loading…
Reference in a new issue