import importlib.machinery import io import json import pathlib from types import SimpleNamespace import unittest from contextlib import redirect_stderr, redirect_stdout SCRIPT = pathlib.Path(__file__).parents[1] / "scripts" / "forgejo-cli" cli = importlib.machinery.SourceFileLoader("forgejo_cli", str(SCRIPT)).load_module() class CliTests(unittest.TestCase): def run_cli(self, args): out, err = io.StringIO(), io.StringIO() with redirect_stdout(out), redirect_stderr(err): try: cli.main(args) except SystemExit as exc: return exc.code, out.getvalue(), err.getvalue() return 0, out.getvalue(), err.getvalue() def plan(self, args): code, out, err = self.run_cli(["--dry-run", "--json"] + args) self.assertEqual(code, 0, err) return json.loads(out) def test_help_needs_no_credentials(self): self.assertEqual(self.run_cli(["--help"])[0], 0) self.assertEqual(self.run_cli(["issue", "--help"])[0], 0) def test_issue_create_plan(self): plan = self.plan(["issue", "create", "--owner", "me", "--repo", "x", "--title", "hello"]) self.assertEqual((plan["method"], plan["path"]), ("POST", "/api/v1/repos/me/x/issues")) self.assertEqual(plan["body"]["title"], "hello") def test_mutation_requires_force(self): code, _, err = self.run_cli(["repo", "create", "--name", "x"]) self.assertNotEqual(code, 0) self.assertIn("mutation", err) def test_live_request_requires_a_server(self): old = cli.DEFAULT_SERVER cli.DEFAULT_SERVER = "" try: code, _, err = self.run_cli(["user", "show"]) finally: cli.DEFAULT_SERVER = old self.assertNotEqual(code, 0) self.assertIn("No Forgejo server selected", err) def test_api_path_guard_and_plan(self): self.assertNotEqual(self.run_cli(["api", "--method", "GET", "--path", "/bad"])[0], 0) plan = self.plan(["api", "--method", "PATCH", "--path", "/api/v1/user/settings", "--query", "theme=dark", "--data", '{"language":"en"}']) self.assertEqual(plan["query"], {"theme": "dark"}) self.assertEqual(plan["body"], {"language": "en"}) def test_representative_groups(self): cases = [ (["pr", "create", "--owner", "me", "--repo", "x", "--title", "t", "--head", "h", "--base", "main"], "POST", "/api/v1/repos/me/x/pulls"), (["release", "create", "--owner", "me", "--repo", "x", "--tag-name", "v2"], "POST", "/api/v1/repos/me/x/releases"), (["content", "update", "--owner", "me", "--repo", "x", "--path", "a b.txt", "--content", "eA==", "--sha", "abc"], "PUT", "/api/v1/repos/me/x/contents/a%20b.txt"), (["hook", "create", "--owner", "me", "--repo", "x", "--url", "https://hook"], "POST", "/api/v1/repos/me/x/hooks"), ] for args, method, path in cases: plan = self.plan(args) self.assertEqual((plan["method"], plan["path"]), (method, path)) def test_merge_hook_and_release_payloads(self): merge = self.plan(["pr", "merge", "--owner", "me", "--repo", "x", "--index", "1", "--style", "squash"]) self.assertEqual(merge["body"], {"Do": "squash"}) hook = self.plan(["hook", "create", "--owner", "me", "--repo", "x", "--url", "https://hook", "--events", "push"]) self.assertEqual(hook["body"]["config"]["url"], "https://hook") self.assertEqual(hook["body"]["type"], "forgejo") release = self.plan(["release", "create", "--owner", "me", "--repo", "x", "--tag-name", "v2", "--name", "Version 2"]) self.assertEqual(release["body"], {"tag_name": "v2", "name": "Version 2"}) def test_repo_creation_needs_no_owner(self): self.assertEqual(self.plan(["repo", "create", "--name", "demo", "--private"])["path"], "/api/v1/user/repos") def test_nested_content_path_and_pagination_plan(self): plan = self.plan(["--page", "2", "--limit", "75", "content", "get", "--owner", "me", "--repo", "x", "--path", "dir/a b.txt"]) self.assertEqual(plan["path"], "/api/v1/repos/me/x/contents/dir/a%20b.txt") self.assertEqual(plan["query"], {"page": 2, "limit": 75}) def test_form_transport_custom_authorization_and_response_metadata(self): class Response: status_code = 200 content = b'{"ok":true}' text = '{"ok":true}' headers = {"Link": '; rel="next"', "X-Total-Count": "51"} def json(self): return {"ok": True} class Requests: RequestException = Exception call = None @classmethod def request(cls, *args, **kwargs): cls.call = (args, kwargs) return Response() original = cli.requests cli.requests = Requests try: args = SimpleNamespace(server="https://forge.example", dry_run=False, verbose=False, user=False) result = cli.Client(args).request( "POST", "/api/v1/repos/me/x/releases/1/assets", headers={"Authorization": "Basic test"}, form={"external_url": "https://example.invalid/file"}, include_response=True) finally: cli.requests = original self.assertEqual(Requests.call[1]["data"], {"external_url": "https://example.invalid/file"}) self.assertEqual(result["data"], {"ok": True}) self.assertEqual(result["headers"]["x-total-count"], "51") if __name__ == "__main__": unittest.main()