@@ -1181,6 +1181,107 @@ mod tests { |
| 1181 | 1181 | let _ = std::fs::remove_file(&socket_path); |
| 1182 | 1182 | } |
| 1183 | 1183 | |
| 1184 | + #[test] |
| 1185 | + fn helper_client_handles_visible_prompt_round_trip() { |
| 1186 | + let socket_path = temp_socket_path(); |
| 1187 | + let listener = UnixListener::bind(&socket_path).expect("bind test socket"); |
| 1188 | + |
| 1189 | + let server = thread::spawn(move || { |
| 1190 | + let (mut stream, _) = listener.accept().expect("accept"); |
| 1191 | + let read_stream = stream.try_clone().expect("clone"); |
| 1192 | + let mut reader = BufReader::new(read_stream); |
| 1193 | + |
| 1194 | + let mut first_line = String::new(); |
| 1195 | + reader.read_line(&mut first_line).expect("read first line"); |
| 1196 | + let first = first_line.trim().to_string(); |
| 1197 | + if first == "operator" { |
| 1198 | + let mut cookie = String::new(); |
| 1199 | + reader.read_line(&mut cookie).expect("read cookie"); |
| 1200 | + } |
| 1201 | + |
| 1202 | + stream |
| 1203 | + .write_all(b"PAM_TEXT_INFO Enter one-time code\n") |
| 1204 | + .expect("write info"); |
| 1205 | + stream |
| 1206 | + .write_all(b"PAM_PROMPT_ECHO_ON Code:\n") |
| 1207 | + .expect("write prompt"); |
| 1208 | + stream.flush().expect("flush prompt"); |
| 1209 | + |
| 1210 | + let mut code = String::new(); |
| 1211 | + reader.read_line(&mut code).expect("read code"); |
| 1212 | + assert_eq!(code.trim(), "123456"); |
| 1213 | + |
| 1214 | + stream.write_all(b"SUCCESS\n").expect("write success"); |
| 1215 | + stream.flush().expect("flush success"); |
| 1216 | + }); |
| 1217 | + |
| 1218 | + let client = HelperSocketClient::new(&socket_path); |
| 1219 | + let mut prompts = FakePrompt { |
| 1220 | + plain_response: PromptResponse::Submitted("123456".to_string()), |
| 1221 | + ..FakePrompt::default() |
| 1222 | + }; |
| 1223 | + |
| 1224 | + let outcome = client |
| 1225 | + .authenticate("operator", "cookie-visible", &mut prompts) |
| 1226 | + .expect("authenticate visible"); |
| 1227 | + assert_eq!(outcome, HelperOutcome::Authorized); |
| 1228 | + assert_eq!(prompts.infos, vec!["Enter one-time code"]); |
| 1229 | + assert_eq!(prompts.success_count, 1); |
| 1230 | + |
| 1231 | + server.join().expect("server join"); |
| 1232 | + let _ = std::fs::remove_file(&socket_path); |
| 1233 | + } |
| 1234 | + |
| 1235 | + #[test] |
| 1236 | + fn helper_client_recovers_after_inline_error_message() { |
| 1237 | + let socket_path = temp_socket_path(); |
| 1238 | + let listener = UnixListener::bind(&socket_path).expect("bind test socket"); |
| 1239 | + |
| 1240 | + let server = thread::spawn(move || { |
| 1241 | + let (mut stream, _) = listener.accept().expect("accept"); |
| 1242 | + let read_stream = stream.try_clone().expect("clone"); |
| 1243 | + let mut reader = BufReader::new(read_stream); |
| 1244 | + |
| 1245 | + let mut first_line = String::new(); |
| 1246 | + reader.read_line(&mut first_line).expect("read first line"); |
| 1247 | + if first_line.trim() == "operator" { |
| 1248 | + let mut cookie = String::new(); |
| 1249 | + reader.read_line(&mut cookie).expect("read cookie"); |
| 1250 | + } |
| 1251 | + |
| 1252 | + stream |
| 1253 | + .write_all(b"PAM_ERROR_MSG Incorrect code, try again\n") |
| 1254 | + .expect("write error"); |
| 1255 | + stream |
| 1256 | + .write_all(b"PAM_PROMPT_ECHO_OFF Password:\n") |
| 1257 | + .expect("write prompt"); |
| 1258 | + stream.flush().expect("flush prompt"); |
| 1259 | + |
| 1260 | + let mut secret = String::new(); |
| 1261 | + reader.read_line(&mut secret).expect("read secret"); |
| 1262 | + assert_eq!(secret.trim(), "correct horse"); |
| 1263 | + |
| 1264 | + stream.write_all(b"SUCCESS\n").expect("write success"); |
| 1265 | + stream.flush().expect("flush success"); |
| 1266 | + }); |
| 1267 | + |
| 1268 | + let client = HelperSocketClient::new(&socket_path); |
| 1269 | + let mut prompts = FakePrompt { |
| 1270 | + secret_response: PromptResponse::Submitted("correct horse".to_string()), |
| 1271 | + ..FakePrompt::default() |
| 1272 | + }; |
| 1273 | + |
| 1274 | + let outcome = client |
| 1275 | + .authenticate("operator", "cookie-inline-error", &mut prompts) |
| 1276 | + .expect("authenticate recovery"); |
| 1277 | + assert_eq!(outcome, HelperOutcome::Authorized); |
| 1278 | + assert_eq!(prompts.errors, vec!["Incorrect code, try again"]); |
| 1279 | + assert_eq!(prompts.success_count, 1); |
| 1280 | + |
| 1281 | + server.join().expect("server join"); |
| 1282 | + let _ = std::fs::remove_file(&socket_path); |
| 1283 | + } |
| 1284 | + |
| 1184 | 1285 | #[test] |
| 1185 | 1286 | fn helper_client_reports_timeout_from_prompt_provider() { |
| 1186 | 1287 | let socket_path = temp_socket_path(); |