Introducing mock-op: The 1Password CLI Emulator
Introduction
Automated testing was a problem I ran into early on with pyonepassword
. The way pyonepassword
works is by driving the 1Password CLI tool, op
, which talks to your 1Password cloud account. This introduces all sorts of problems for testing, including:
The
op
command requires authentication for most operationsThe performance implications of
op
's live authentication and interaction with the 1Password cloudLack of testing isolation, since a live 1Password account pre-populated with data is required
I needed a tool that would pretend to be op
but would:
Use self-contained data, ideally added to the tested project's version control
Not require network connectivity
Not, under any circumstances, require console interaction
Produce the same output every time given a particular input
So I developed mock-op
, which does the above, PLUS provides a DVR-style capability to observe and record op
to play back its behaviors.
Overview
The mock-op
package comes two significant parts:
A
mock-op
console utility that plays back responses identical to the realop
command's output for a given set of command-line arguments. Additionally, the same exit statuses are returned so that errors can be handled appropriately.A
response-generator
utility to executeop
under various scenarios and record its responses for use bymock-op
Usage and Installation
Install using pip
from GitHub:
$ pip install git+https://github.com/zcutlip/mock-op
mock-op
CLI Tool
For mock-op
to simulate the real op
it needs a directory of responses to look up and playback. The anatomy of the response directory is discussed in the README for mock-cli-framework
, but below is an example:
JSON Dictionary of command invocations:
{
"meta": {
"response_dir": "./responses"
},
"commands": {
"item|get|Example Login 1|--vault|Test Data": {
"exit_status": 0,
"stdout": "output",
"stderr": "error_output",
"name": "item-get-[example-login-1]-[vault-test-data]"
},
"item|get|nok7367v4vbsfgg2fczwu4ei44|--fields|username,password": {
"exit_status": 0,
"stdout": "output",
"stderr": "error_output",
"name": "item-get-[example-login-2]-[fields-username-password]"
},
"item|get|Invalid Item": {
"exit_status": 1,
"stdout": "output",
"stderr": "error_output",
"name": "item-get-[invalid-item]"
}
}
}
An on-disk repository of response files:
responses
├── item-get-[example-login-1]-[vault-test-data]
│ ├── error_output
│ └── output
├── item-get-[example-login-2]-[fields-username-password]
│ ├── error_output
│ └── output
└── item-get-[invalid-item]
├── error_output
└── output
With that directory in place, the mock-op
utility will look up responses and exit status based on the set of command-line arguments given.
Let's emulate an "item get" for item UUID nok7367v4vbsfgg2fczwu4ei44
, requesting fields 'username' and 'password':
$ mock-op item get nok7367v4vbsfgg2fczwu4ei44 --fields username,password
{"password":"weak password","username":"janedoe123"}
The output is the exact JSON that op
would give.
Let's try an "item get" for an item that doesn't exist:
$ mock-op item get "Invalid Item"
[ERROR] 2021/02/03 13:39:42 "Invalid Item" doesn't seem to be an item. Specify
the item with its UUID, name, or domain.
$ # check the exit status...
$ echo $?
1
$
The output on stderr
is the same error message op
would give, and the exit status is 1
, as expected.
Automated Response Generation
The response file & directory structure was designed to be fairly straightforward so that one could create it by hand or easily script it. However, mock-op
comes with a tool to generate responses. You provide a configuration file, and it will connect to your 1Password account (using the real op
tool), perform the queries, and record the responses.
Note: response generation requires you to install the
pyonepassword
Python package. It's responsible for driving theop
command under the hood so its responses can be captured.It's only required during response generation and is not required for subsequent use of
mock-op
.It can be found in PyPI and installed via:
pip3 install pyonepassword
.
Here's an example configuration file for generating responses. Note that the invalid item definition has an expected-return
value of 1. This tells response-generator
that an error is expected and should be captured rather than failing.
[MAIN]
config-path = ./tests/config/mock-op
response-path = responses
response-dir-file = response-directory.json
[whoami]
type=whoami
[item-get-example-login-1-vault-test-data]
type=item-get
item_identifier = Example Login 1
vault = Test Data
[item-get-invalid]
type = item-get
item_identifier = Invalid Item
expected-return = 1
Then you can run response-generator
and have it create your response directory:
❱ response-generator ./examples/example-response-generation.cfg
Running: op --version
Running: op --format json account list
Running: op --format json whoami
Running: op signin --raw
Running: op --format json whoami
Signed in as User ID: *********************ERLHI
About to run: op --format json whoami
About to run: op --format json item get 'Example Login 1' --vault 'Test Data'
About to run: op --format json item get 'Invalid Item'
The response generator records the output, standard error, and exit status of each invocation of op
. It then records that in the JSON response directory. We end up with a directory that looks like:
$ tree tests/config/mock-op
tests/config/mock-op
├── response-directory.json
└── responses
├── item-get-example-login-1-vault-test-data
│ ├── error_output
│ └── output
├── item-get-invalid
│ ├── error_output
│ └── output
└── whoami
├── error_output
└── output
Limitations
The main limitations are:
mock-op
can't simulate various authentication flows- It can simulate being authenticated or not, resulting in failures where appropriate
mock-op
can't currently simulateop
operations that change stateIn other words, if you need to simulate a successful "item get," followed by an "item delete," and finally an "item get" that fails,
mock-op
can't currently do thisSupport for state-changing operations is currently under development, so stay tuned!
Summary
If you have a situation where you need to simulate the op
command, but that's impractical for performance or other reasons, mock-op
may be for you.
My pyonepassword
project has over 455 automated test cases, most of which use mock-op
in some way. You Can browse that real-world mock-op
configuration here.
Check out mock-op
at its GitHub project page.
Subscribe to my newsletter
Read articles from Zachary Cutlip directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by