annotate testharness/src/bin/testharness.rs @ 171:e27c5c667a5a

Create full new types for return code and flags, separate end to end. This plumbs the ReturnCode and RawFlags types through the places where we call into or are called from PAM. Also adds Sun documentation to the project.
author Paul Fisher <paul@pfish.zone>
date Fri, 25 Jul 2025 20:52:14 -0400
parents 0cabe7b94a4f
children 6727cbe56f4a
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
104
a2676475e86b Create the very start of a test suite.
Paul Fisher <paul@pfish.zone>
parents:
diff changeset
1 //! The actual program which runs the tests.
a2676475e86b Create the very start of a test suite.
Paul Fisher <paul@pfish.zone>
parents:
diff changeset
2
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
3 use nonstick::conv::Exchange;
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
4 use nonstick::items::Items;
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
5 use nonstick::libpam::TransactionBuilder;
166
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
6 use nonstick::{
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
7 AuthnFlags, AuthtokFlags, Conversation, ErrorCode, LibPamTransaction, PamShared, Transaction,
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
8 };
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
9 use std::cell::Cell;
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
10 use std::ffi::OsString;
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
11 use std::os::unix::ffi::OsStrExt;
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
12
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
13 fn main() {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
14 test_wrong_user();
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
15 test_wrong_password();
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
16 test_correct();
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
17 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
18
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
19 #[derive(Debug, Default)]
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
20 struct TestHarness {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
21 username_requested: Cell<bool>,
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
22 wrong_username: bool,
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
23 wrong_password: bool,
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
24 changing_password: Cell<bool>,
171
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
25 change_prompt_count: Cell<usize>,
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
26 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
27
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
28 impl Conversation for &TestHarness {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
29 fn communicate(&self, messages: &[Exchange]) {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
30 if let [only_msg] = messages {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
31 match only_msg {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
32 Exchange::Prompt(p) => {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
33 if self.username_requested.get() {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
34 panic!("username already requested!")
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
35 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
36 if self.wrong_username {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
37 p.set_answer(Ok(OsString::from("not-right")))
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
38 } else {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
39 p.set_answer(Ok(OsString::from("initial")))
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
40 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
41 self.username_requested.set(true)
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
42 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
43 Exchange::MaskedPrompt(p) => {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
44 let answer = if self.changing_password.get() {
171
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
45 let prompt_count = self.change_prompt_count.get();
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
46 eprintln!("CHANGING PASSWORD PROMPT {prompt_count}");
167
0cabe7b94a4f Check for old_authtok in change_authtok to emulate real behavior.
Paul Fisher <paul@pfish.zone>
parents: 166
diff changeset
47 eprintln!("-> {p:?}");
171
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
48 self.change_prompt_count.set(prompt_count + 1);
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
49 // When changing passwords after logging in, Sun PAM
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
50 // uses the existing authtok that was just entered as
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
51 // the old_authtok. Other PAMs prompt the user to enter
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
52 // their existing password again.
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
53 let responses: &[&str] = if cfg!(pam_impl = "Sun") {
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
54 &["mistake", "mismatch", "acceptable", "acceptable"]
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
55 } else {
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
56 &[
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
57 "old token!",
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
58 "mistake",
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
59 "mismatch",
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
60 "old token!",
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
61 "acceptable",
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
62 "acceptable",
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
63 ]
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
64 };
e27c5c667a5a Create full new types for return code and flags, separate end to end.
Paul Fisher <paul@pfish.zone>
parents: 167
diff changeset
65 responses[prompt_count]
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
66 } else if self.wrong_password {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
67 "bogus"
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
68 } else {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
69 "valid"
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
70 };
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
71 p.set_answer(Ok(OsString::from(answer)));
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
72 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
73 Exchange::Error(e) if self.changing_password.get() => e.set_answer(Ok(())),
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
74 other => panic!("Unknown message {other:?}!"),
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
75 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
76 } else {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
77 for msg in messages {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
78 match msg {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
79 Exchange::Info(i) => i.set_answer(Ok(())),
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
80 Exchange::Error(e) => e.set_answer(Ok(())),
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
81 Exchange::Prompt(p) => match p.question().as_bytes() {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
82 b"How many?" => p.set_answer(Ok(OsString::from("123"))),
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
83 _ => p.set_answer(Err(ErrorCode::ConversationError)),
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
84 },
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
85 Exchange::MaskedPrompt(p) => match p.question().as_bytes() {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
86 b"Where?" => p.set_answer(Ok(OsString::from("abc"))),
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
87 _ => p.set_answer(Err(ErrorCode::ConversationError)),
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
88 },
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
89 other => other.set_error(ErrorCode::Abort),
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
90 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
91 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
92 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
93 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
94 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
95
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
96 impl TestHarness {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
97 fn start(&self) -> LibPamTransaction<&Self> {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
98 TransactionBuilder::new_with_service("nonstick-testharness")
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
99 .build(self)
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
100 .expect("expected build success")
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
101 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
102 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
103
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
104 fn test_wrong_user() {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
105 let harness = TestHarness {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
106 wrong_username: true,
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
107 ..Default::default()
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
108 };
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
109 let mut tx = harness.start();
166
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
110 let auth = tx.authenticate(AuthnFlags::empty());
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
111 assert_eq!(auth, Err(ErrorCode::UserUnknown));
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
112 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
113
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
114 fn test_wrong_password() {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
115 let harness = TestHarness {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
116 wrong_password: true,
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
117 ..Default::default()
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
118 };
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
119 let mut tx = harness.start();
166
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
120 let auth = tx.authenticate(AuthnFlags::empty());
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
121 assert_eq!(auth, Err(ErrorCode::AuthenticationError));
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
122 }
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
123
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
124 fn test_correct() {
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
125 let harness = TestHarness::default();
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
126 let mut tx = harness.start();
166
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
127 tx.authenticate(AuthnFlags::empty()).unwrap();
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
128 assert_eq!(tx.items().user().unwrap().unwrap(), "updated-in-process");
166
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
129 let result = tx.account_management(AuthnFlags::empty());
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
130 assert_eq!(result, Err(ErrorCode::NewAuthTokRequired));
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
131 harness.changing_password.set(true);
166
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
132 let change = tx.change_authtok(AuthtokFlags::CHANGE_EXPIRED_AUTHTOK);
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
133 assert_eq!(change, Err(ErrorCode::TryAgain));
166
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
134 tx.change_authtok(AuthtokFlags::CHANGE_EXPIRED_AUTHTOK)
2f5913131295 Separate flag/action flags into flags and action.
Paul Fisher <paul@pfish.zone>
parents: 163
diff changeset
135 .unwrap();
163
a75a66cb4181 Add end-to-end tests; fix issues found by tests.
Paul Fisher <paul@pfish.zone>
parents: 104
diff changeset
136 }