feat: unit tests#20
Open
HarishChandran3304 wants to merge 6 commits into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a comprehensive unit/integration-style test suite for frappe_openapi’s OpenAPI generator and lifecycle hooks, and introduces CI workflows to run those tests (plus lint/build checks) consistently in GitHub Actions. This strengthens regression protection around spec generation, app enablement logic, and install/uninstall behavior.
Changes:
- Added ~88 tests covering AST/docstring parsing, schema generation, app metadata/bulk generation, and settings/docs context behavior.
- Added GitHub Actions workflows for unit tests, linters, and bench build verification, plus a standard PR template.
- Included small hardening/cleanup changes (semgrep suppressions for non-user-controlled file paths; demo API error message translation + metadata response fix).
Reviewed changes
Copilot reviewed 14 out of 19 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
frappe_openapi/tests/www/test_docs.py |
Tests www/docs.py::get_context app filtering/default selection behavior. |
frappe_openapi/tests/www/__init__.py |
Marks tests.www as a package for test discovery/imports. |
frappe_openapi/tests/test_generate_api_docs.py |
Core test suite for generator helpers and OpenAPI generation/bulk write logic. |
frappe_openapi/tests/setup/test_uninstall.py |
Tests uninstall hook behavior (Custom Field cleanup + error logging). |
frappe_openapi/tests/setup/test_install.py |
Tests install hook behavior (calls + error logging). |
frappe_openapi/tests/setup/__init__.py |
Marks tests.setup as a package. |
frappe_openapi/tests/doctype/openapi_settings/test_openapi_settings.py |
Tests OpenAPISettings.on_update enqueues regeneration correctly. |
frappe_openapi/tests/doctype/openapi_settings/__init__.py |
Marks doctype test package. |
frappe_openapi/tests/doctype/__init__.py |
Marks tests.doctype as a package. |
frappe_openapi/tests/config/test_create_app_list.py |
Tests Custom Field lifecycle for per-app OpenAPI enable flags. |
frappe_openapi/tests/config/__init__.py |
Marks tests.config as a package. |
frappe_openapi/tests/__init__.py |
Adds shared AST/temp-package helpers used across tests. |
frappe_openapi/frappe_openapi/generate_api_docs.py |
Minor internal adjustments (string join refactor; semgrep suppressions on controlled paths). |
frappe_openapi/api/examples.py |
Improves demo endpoint behavior (translated throw message; correct metadata inclusion) and adds semgrep suppressions for intended guest endpoints. |
.semgrepignore |
Removes an unrelated ignored path entry. |
.github/workflows/unit-tests.yml |
Adds unit-test CI workflow (bench setup + run-tests). |
.github/workflows/linters.yml |
Adds linting workflow using pre-commit on changed files. |
.github/workflows/build-test.yml |
Adds bench build verification workflow. |
.github/PULL_REQUEST_TEMPLATE.md |
Adds a standard PR template. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #19 - commits reworded from
tests:totest:and action versions bumped.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 #