feat: unit tests#19
Conversation
There was a problem hiding this comment.
⚠️ Not ready to approve
CI workflows contain a non-existent action version (actions/cache@v5) and the build-test workflow doesn’t pin a compatible Frappe branch, both of which can break CI determinism.
Pull request overview
Adds a comprehensive test suite for frappe_openapi’s OpenAPI generation and lifecycle hooks, and introduces CI workflows to run unit tests and linters in GitHub Actions. Also includes small, targeted changes in the generator and demo endpoints to address pre-existing lint/semgrep findings and a demo response bug.
Changes:
- Added 88 integration/unit tests covering AST helpers, docstring parsing, schema generation, bulk generation, lifecycle hooks, and website docs context.
- Introduced GitHub Actions workflows for unit tests, linters, and a bench build sanity-check.
- Updated generator and demo endpoints with small refactors and semgrep suppressions for known-safe file operations / intentionally guest-accessible demos.
File summaries
| File | Description |
|---|---|
frappe_openapi/tests/test_generate_api_docs.py |
Main test suite covering AC sections 1–7 for the generator pipeline. |
frappe_openapi/tests/__init__.py |
Shared test helpers for AST parsing and temporary app package creation. |
frappe_openapi/tests/www/test_docs.py |
Tests for www.docs.get_context app filtering/default selection behavior. |
frappe_openapi/tests/www/__init__.py |
Package marker for tests.www. |
frappe_openapi/tests/config/test_create_app_list.py |
Tests for Custom Field lifecycle creation/removal logic. |
frappe_openapi/tests/config/__init__.py |
Package marker for tests.config. |
frappe_openapi/tests/setup/test_install.py |
Tests for after_install behavior and error logging. |
frappe_openapi/tests/setup/test_uninstall.py |
Tests for before_uninstall behavior and error logging. |
frappe_openapi/tests/setup/__init__.py |
Package marker for tests.setup. |
frappe_openapi/tests/doctype/openapi_settings/test_openapi_settings.py |
Tests for OpenAPISettings.on_update enqueue behavior. |
frappe_openapi/tests/doctype/openapi_settings/__init__.py |
Package marker for OpenAPI Settings doctype tests. |
frappe_openapi/tests/doctype/__init__.py |
Package marker for tests.doctype. |
frappe_openapi/frappe_openapi/generate_api_docs.py |
Minor refactor + semgrep suppressions for controlled file operations. |
frappe_openapi/api/examples.py |
Fixes demo auth metadata behavior and improves i18n/semgrep annotations. |
.github/workflows/unit-tests.yml |
Adds CI job to run bench-based unit tests. |
.github/workflows/linters.yml |
Adds/updates pre-commit linter workflow. |
.github/workflows/build-test.yml |
Adds bench init + dependency/app fetch workflow for build sanity checks. |
.github/PULL_REQUEST_TEMPLATE.md |
Adds a standard PR template. |
.semgrepignore |
Removes an incorrect ignore entry and normalizes markdown ignore. |
Copilot's findings
- Files reviewed: 14/19 changed files
- Comments generated: 2
Note
Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
opening new PR to resolve semantic commits check |
Description
PR to add unit tests for
frappe_openapiand set up the missing unit-tests CI workflow.ref: App Spec
88 tests, all passing locally via
bench --site <site> run-tests --app frappe_openapi.Test Cases
AST helpers and type-map constants
DEFAULT_METHODS == ["get", "post", "put", "delete"]_PLACEHOLDER_TYPE_MAPmapsfloat/number → number,int/integer → integer,str/string → string,bool/boolean → boolean,list/array → array (items: {}),dict/object → object_PYTHON_TO_OPENAPImaps Python annotation names to OpenAPI type strings (includingbytes → string)find_python_filesreturns every.pyfile under the base path recursively and excludes non-.pyfilesget_decorator_info([])returns(DEFAULT_METHODS, False)get_decorator_infoparsesmethods=["POST"]to["post"](lowercased list)get_decorator_infoparsesmethods="POST"(single string) to["post"]get_decorator_infoparsesallow_guest=Trueand combines withmethods=correctlyDocstring parsing
_parse_typed_blockreturnsNonewhen no"key": <type>pairs are present_parse_typed_blockparses placeholder pairs into a tagged{"__placeholder_schema__": ...}dict_parse_typed_blockfalls back to{"type": "string"}for unknown placeholder typesextract_returns_from_docstringreturnsNoneforNone/empty inputextract_returns_from_docstringmatchesReturns:andReturn:(case-insensitive)extract_returns_from_docstringparses a balanced{...}block as JSON (single-quotes normalised)extract_returns_from_docstringdispatches to_parse_typed_blockfor<type>placeholder dictsextract_returns_from_docstringreturns the raw block string when JSON parsing failsextract_returns_from_docstringstops at the next top-level section header (e.g.Raises:) rather than consuming the rest of the docstringparse_docstring_argsreturns{}forNone/empty/no-Args inputparse_docstring_argsparsesname (int, optional): descinto a schema withrequired=Falseparse_docstring_argsmarks parameters withoutoptionalin the type info as requiredparse_docstring_argsjoins multi-line continuation descriptions with a single spaceparse_docstring_argsmapslisttype info to{"type": "array", "items": {}}Type annotation -> OpenAPI schema (
get_openapi_type)Noneinput returns{"type": "string"}ast.Name("int")returns{"type": "integer"};ast.Name("list")returns array-with-items; unknown names fall back to stringast.Constant(None)returns{"nullable": True}(notypekey)Optional[int]returns{"type": "integer", "nullable": True}List[int]returns{"type": "array", "items": {"type": "integer"}}Dict[str, int]returns{"type": "object"}Tuple[int, str]returns{"type": "array", "items": {}}Union[int, None]returns{"type": "integer", "nullable": True}Union[int, str]returns{"oneOf": [<int>, <str>]}Union[int, str, None]returns oneOf + nullableint | Nonereturns{"type": "integer", "nullable": True}int | str | Nonereturns oneOf + nullableast.Attribute(e.g.typing.int) recurses via a syntheticast.NameResponse schema builder (
build_response_schema)(None, None)returns{"message": {"type": "string"}}under an object wrapper(None, [])returns{"message": {"type": "array", "items": {}}}(None, {})returns{"message": {"type": "object"}}propertiesfrom keys (bool/int/float/str/list/dict mapped to OpenAPI types)intannotation with no example yields{"message": {"type": "integer"}}__placeholder_schema__example and wraps it undermessageFunction metadata extraction (
parse_functions_from_file)whitelistname,params,doc,methods,allow_guest,return_annotation,returns_example)selfandclsfromparamsrequired = i < n_requiredfromlen(args) - len(defaults)type_schemain priority order: annotation →Args:block →{"type": "string"}defaultPer-app OpenAPI generation (
generate_openapi_static){}whenimportlib.util.find_specreturnsNoneopenapi="3.0.0",info,paths={},servers=[{"url": <frappe.utils.get_url()>}]/api/method/{app}.{module_path}.{func_name}withtags=[parent_module]summary; rest becomesdescription(only set when non-empty)params(each withname,in="query",required,schema, optionaldescription)application/x-www-form-urlencodedrequest body withpropertiesandrequiredlistsrequestBodykeyrequestBody.required=FalserequestBody.required=Truesecurityis[{"TokenAuth": []}, {"bearerAuth": []}](two separate scheme requirements, not one merged dict)message(number→0.0,integer→0,string→"",boolean→False,array→[],object→{}){"message": <returns_example>}components.securitySchemesis added when at least one operation is non-guest; includes TokenAuth (apiKey, header, Authorization) and bearerAuth (http bearer,bearerFormat="JWT")componentsis omitted when every operation hasallow_guest=Trueopenapi["tags"]is a sorted list of{"name": <module>}dictsApp metadata and bulk generation
get_app_title_and_versionreturns(hooks.app_title, module.__version__)when both are importableapp_namewhen the hooks import fails orapp_titleis missing"1.0.0"when the app import fails or__version__is missinggenerate_openapi_for_all_appswrites oneopenapi_<app>.jsonper enabled app under<site>/public/files/openapi/info.titleandinfo.versionon the generator's output with the values fromget_app_title_and_versionopenapi_<app>.jsonfor disabled appsfrappe.log_errorwith title"OpenAPI Deletion Error"frappe.log_errorwith title"OpenAPI Generation Error"update_progress_bar("Generating OpenAPI spec", i, total)wheretotal = len(enabled_apps)Custom Field lifecycle and DocType controller
create_openapi_app_fieldsexcludesfrappe_openapifrom the set of apps it processesdt="OpenAPI Settings",fieldtype="Check",label=<app title>,insert_after="generate_openapi_specification_for_selected_apps_section",default="1"apps_to_process"OpenAPI Custom Field Removal Error""OpenAPI Custom Field Creation Error"after_installcallscreate_openapi_app_fieldsand swallows any exception with log title"OpenAPI After Install Error"before_uninstalldeletes every Custom Field wheredt="OpenAPI Settings", swallowing per-field and top-level exceptions with log title"OpenAPI Uninstall Error"OpenAPISettings.on_updateenqueuesgenerate_openapi_for_all_appson thelongqueue withenqueue_after_commit=TrueWebsite docs context (
www/docs.py::get_context)context.appsandcontext.app_titlesonly with apps whose settings flag is truthycontext.default_appis the first element ofenabled_appscontext.default_appis""when no apps are enabledRelevant Technical Choices
ast.parse(no mocks for the AST helpers, docstring parsing, type-annotation conversion, response-schema building, or function-metadata extraction).generate_openapi_static) construct tempdir-based Python packages via atemp_app_package(name, files)helper intests/__init__.py, then patchimportlib.util.find_specto point the generator at the tempdir. The generator walks real files on disk.frappe.get_single,frappe.get_installed_apps,frappe.get_site_path, and the consumer-module-importedupdate_progress_barto control progress and skip the real progress bar.frappe.get_installed_apps,frappe.get_all,frappe.get_doc,frappe.delete_doc,frappe.log_error,frappe.get_single) at the consumer module paths. A follow-up sweep (Phase 2.5) will convert these to real Custom Field fixtures.chore: address pre-existing semgrep and ruff findingscommit that fixes (or rationalises with# nosemgrepcomments) the pre-existing issues that were blocking any commit on this repo, plus a real bug fix inapi/examples.pywhere_check_demo_enabledused an untranslatedfrappe.throwand the demo'sinclude_metadataflag had no effect ({} if include_metadata else {}returned{}either way).Testing Instructions
Additional Information:
This PR contains 4 commits on
feat/unit-tests:chore: address pre-existing semgrep and ruff findings— wrapsfrappe.throwuser-facing message infrappe._(), adds# nosemgrep(with justification) to the three intentionally guest-accessible demo endpoints and the two file-traversal warnings on controlled paths, refactorsfilter() + comprehensioninto a single generator expression, replaces 15 en-dashes in comments/docstrings with hyphens, and fixes the useless{} if include_metadata else {}indemo_authenticated(now the response'smetadatakey is only set wheninclude_metadata=True).chore: sync tooling drift— adds the standard.github/PULL_REQUEST_TEMPLATE.md,build-test.yml, andlinters.yml, and removes a strayfrappe_skeleton/__init__.pyline from.semgrepignore.tests: add full suite for openapi spec generator and lifecycle hooks— 88 tests across 6 test files mirroring the source tree.ci: add unit tests workflow— addsunit-tests.ymlmatching the Phase 2 pattern (ubuntu-latest, MariaDB 10.6, Redis × 2, Python 3.14, Node 24,bench run-tests --app frappe_openapi).Screenshot/Screencast
Checklist
Fixes #