diff --git a/tools/nid_names.py b/tools/nid_names.py index a633f94dd..2cb0d756e 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -160,6 +160,9 @@ def main(argv=None): arguments = sys.argv[1:] if argv is None else argv args = parser.parse_args(nid_arguments(arguments, options)) requested_nids = any(option_name(argument, options) == "--nid" for argument in arguments) + for nid in args.nid or []: + if re.fullmatch(r"[A-Za-z0-9+\-]{11}", nid) is None: + parser.error(f"invalid NID {nid!r}: expected 11 characters from A-Z, a-z, 0-9, + or -") db_path = Path(args.db) if args.db else DEFAULT_CACHE try: diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 66b18f5df..41a8f5118 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -94,6 +94,38 @@ class DatabaseCacheTests(unittest.TestCase): class NidArgumentTests(unittest.TestCase): options = ["-h", "--help", "--db", "--nid", "--json"] + def test_invalid_nids_fail_before_database_or_source_access(self): + for nid in ("short", "ABCDEFGHIJKL", "ABCDEFGHIJ/", "ABCDEFGHIJ_", "ABCDEFGHIJ\u00e9", "", "--jsooon"): + with self.subTest(nid=nid): + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()) as error, \ + patch.object(nid_names, "load_db", return_value={}) as database, \ + patch.object(nid_names, "collect_unknowns") as sources, \ + patch.object(nid_names, "collect_real_names", return_value=set()), \ + self.assertRaises(SystemExit) as exited: + nid_names.main(["--nid=" + nid, "--json"]) + self.assertEqual(exited.exception.code, 2) + self.assertIn("invalid NID", error.getvalue()) + database.assert_not_called() + sources.assert_not_called() + + def test_option_typo_in_nid_list_is_not_an_unknown_success(self): + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()) as error, \ + patch.object(nid_names, "load_db", return_value={}) as database, \ + patch.object(nid_names, "collect_real_names", return_value=set()), self.assertRaises(SystemExit) as exited: + nid_names.main(["--nid", "ZjtRqSMJwdw", "--jsooon"]) + self.assertEqual(exited.exception.code, 2) + self.assertIn("--jsooon", error.getvalue()) + database.assert_not_called() + + def test_valid_unknown_nid_remains_a_successful_lookup(self): + output = io.StringIO() + with patch.object(nid_names, "load_db", return_value={}), \ + patch.object(nid_names, "collect_real_names", return_value=set()), \ + contextlib.redirect_stdout(output): + self.assertEqual(nid_names.main(["--nid=AAAAAAAAAA+", "--json"]), 0) + row, = json.loads(output.getvalue())["rows"] + self.assertEqual((row["nid"], row["status"]), ("AAAAAAAAAA+", "unknown")) + def normalize(self, arguments): return nid_names.nid_arguments(arguments, self.options)