diff --git a/tests/integration/static_keys.py b/tests/integration/static_keys.py new file mode 100644 index 0000000..3340dda --- /dev/null +++ b/tests/integration/static_keys.py @@ -0,0 +1,41 @@ +"""Static fixtures for the integration suite: throwaway OpenPGP public keys. + +Both keys were generated offline for a fake identity (``Aegis IT Fixture N`` with an +``example.test`` address); only the public halves are committed. Gitea accepts a GPG key +only when one of its identities carries an activated email address of the account, so the +tests first add the matching address to the account (``GPG_EMAILS``). +""" + +from __future__ import annotations + +GPG_EMAILS = ( + "aegis-it-fixture-1@example.test", + "aegis-it-fixture-2@example.test", +) + +GPG_PUBLIC_KEYS = ( + """\ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEas+oDBYJKwYBBAHaRw8BAQdA7yjFLeF4uUtwBS9tSHRfz1p3CO3zX5G01SqY +Kq4Ebuu0NEFlZ2lzIElUIEZpeHR1cmUgMSA8YWVnaXMtaXQtZml4dHVyZS0xQGV4 +YW1wbGUudGVzdD6IkwQTFgoAOxYhBLEDZkoa8oLNuNJlafBqtfhHw71yBQJqz6gM +AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEPBqtfhHw71yXN8BAPZ7 +ShczPrCQqX+YN/YP5gU+Cnfnopt3yo66chupK6ADAQD3fdC2kwi8DQQGPdeLb7qC +TPcxXQWSnXeVTr3EQKFFAQ== +=KK0/ +-----END PGP PUBLIC KEY BLOCK----- +""", + """\ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEas+oDBYJKwYBBAHaRw8BAQdAfUIIJtFAtjCsr39V+TnKqtUrWINy+9KsJdgq +4CWgjnS0NEFlZ2lzIElUIEZpeHR1cmUgMiA8YWVnaXMtaXQtZml4dHVyZS0yQGV4 +YW1wbGUudGVzdD6IkwQTFgoAOxYhBFk8Jp2uNta24tbmPees4IAGb9G9BQJqz6gM +AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEOes4IAGb9G9880BAJwB +Kl2G944vYVF2XXS3S/PspgOj9Gcm6Q8T/07MPLTIAP0TUTvS2gg2CF3GXp7YCgUf +0hZziR0B882mz4IWtfroAQ== +=teCc +-----END PGP PUBLIC KEY BLOCK----- +""", +) diff --git a/tests/integration/sweep_baseline.json b/tests/integration/sweep_baseline.json new file mode 100644 index 0000000..caa3947 --- /dev/null +++ b/tests/integration/sweep_baseline.json @@ -0,0 +1,543 @@ +{ +"gitea": "28.1.0", +"comment": "Expected outcome class per tool in the sweep: 2xx, or the exact status of a documented non-2xx or allowance. Regenerate with AEGIS_IT_WRITE_BASELINE=1.", +"tools": { + "gitea_actions_approve_workflow_run": 409, + "gitea_actions_cancel_workflow_run": "2xx", + "gitea_actions_create_org_variable": "2xx", + "gitea_actions_create_repo_variable": "2xx", + "gitea_actions_create_user_variable": "2xx", + "gitea_actions_delete_action_run": 400, + "gitea_actions_delete_artifact": 404, + "gitea_actions_delete_org_runner": 404, + "gitea_actions_delete_org_secret": "2xx", + "gitea_actions_delete_org_variable": "2xx", + "gitea_actions_delete_repo_runner": 404, + "gitea_actions_delete_repo_secret": "2xx", + "gitea_actions_delete_repo_variable": "2xx", + "gitea_actions_delete_user_runner": 404, + "gitea_actions_delete_user_secret": "2xx", + "gitea_actions_delete_user_variable": "2xx", + "gitea_actions_disable_workflow": "2xx", + "gitea_actions_dispatch_workflow": "2xx", + "gitea_actions_download_artifact": 404, + "gitea_actions_download_run_job_logs": 404, + "gitea_actions_enable_workflow": "2xx", + "gitea_actions_force_cancel_workflow_run": 409, + "gitea_actions_get_artifact": 404, + "gitea_actions_get_artifacts": "2xx", + "gitea_actions_get_artifacts_of_run": "2xx", + "gitea_actions_get_org_runner": 404, + "gitea_actions_get_org_runners": "2xx", + "gitea_actions_get_org_variable": "2xx", + "gitea_actions_get_org_variables_list": "2xx", + "gitea_actions_get_org_workflow_jobs": "2xx", + "gitea_actions_get_org_workflow_runs": "2xx", + "gitea_actions_get_repo_runner": 404, + "gitea_actions_get_repo_runners": "2xx", + "gitea_actions_get_repo_variable": "2xx", + "gitea_actions_get_repo_variables_list": "2xx", + "gitea_actions_get_user_runner": 404, + "gitea_actions_get_user_runners": "2xx", + "gitea_actions_get_user_variable": "2xx", + "gitea_actions_get_user_variables_list": "2xx", + "gitea_actions_get_user_workflow_jobs": "2xx", + "gitea_actions_get_user_workflow_runs": "2xx", + "gitea_actions_get_workflow": "2xx", + "gitea_actions_get_workflow_job": "2xx", + "gitea_actions_get_workflow_run": "2xx", + "gitea_actions_get_workflow_run_attempt": "2xx", + "gitea_actions_get_workflow_run_logs": 404, + "gitea_actions_get_workflow_runs": "2xx", + "gitea_actions_list_action_tasks": "2xx", + "gitea_actions_list_repository_workflows": "2xx", + "gitea_actions_list_workflow_jobs": "2xx", + "gitea_actions_list_workflow_run_attempt_jobs": "2xx", + "gitea_actions_list_workflow_run_jobs": "2xx", + "gitea_actions_list_workflow_runs": "2xx", + "gitea_actions_org_create_runner_registration_token": "2xx", + "gitea_actions_org_list_secrets": "2xx", + "gitea_actions_repo_create_runner_registration_token": "2xx", + "gitea_actions_repo_list_secrets": "2xx", + "gitea_actions_rerun_failed_workflow_run": "2xx", + "gitea_actions_rerun_workflow_job": 404, + "gitea_actions_rerun_workflow_run": 400, + "gitea_actions_update_org_runner": 404, + "gitea_actions_update_org_secret": "2xx", + "gitea_actions_update_org_variable": "2xx", + "gitea_actions_update_repo_runner": 404, + "gitea_actions_update_repo_secret": "2xx", + "gitea_actions_update_repo_variable": "2xx", + "gitea_actions_update_user_runner": 404, + "gitea_actions_update_user_secret": "2xx", + "gitea_actions_update_user_variable": "2xx", + "gitea_actions_user_create_runner_registration_token": "2xx", + "gitea_admin_add_user_badges": 500, + "gitea_admin_adopt_repository": 404, + "gitea_admin_create_hook": "2xx", + "gitea_admin_create_org": "2xx", + "gitea_admin_create_public_key": "2xx", + "gitea_admin_create_repo": "2xx", + "gitea_admin_create_runner_registration_token": "2xx", + "gitea_admin_create_user": "2xx", + "gitea_admin_cron_list": "2xx", + "gitea_admin_cron_run": "2xx", + "gitea_admin_delete_hook": "2xx", + "gitea_admin_delete_runner": 404, + "gitea_admin_delete_unadopted_repository": 404, + "gitea_admin_delete_user": "2xx", + "gitea_admin_delete_user_badges": "2xx", + "gitea_admin_delete_user_public_key": "2xx", + "gitea_admin_edit_hook": "2xx", + "gitea_admin_edit_user": "2xx", + "gitea_admin_get_all_emails": "2xx", + "gitea_admin_get_all_orgs": "2xx", + "gitea_admin_get_hook": "2xx", + "gitea_admin_get_runner": 404, + "gitea_admin_get_runners": "2xx", + "gitea_admin_list_hooks": "2xx", + "gitea_admin_list_packages": "2xx", + "gitea_admin_list_user_badges": "2xx", + "gitea_admin_list_workflow_jobs": "2xx", + "gitea_admin_list_workflow_runs": "2xx", + "gitea_admin_rename_user": "2xx", + "gitea_admin_search_emails": "2xx", + "gitea_admin_search_users": "2xx", + "gitea_admin_unadopted_list": "2xx", + "gitea_admin_update_runner": 404, + "gitea_api_request": "2xx", + "gitea_hook_org_create_hook": "2xx", + "gitea_hook_org_delete_hook": "2xx", + "gitea_hook_org_edit_hook": "2xx", + "gitea_hook_org_get_hook": "2xx", + "gitea_hook_org_list_hooks": "2xx", + "gitea_hook_repo_create_hook": "2xx", + "gitea_hook_repo_delete_git_hook": "2xx", + "gitea_hook_repo_delete_hook": "2xx", + "gitea_hook_repo_edit_git_hook": "2xx", + "gitea_hook_repo_edit_hook": "2xx", + "gitea_hook_repo_get_git_hook": "2xx", + "gitea_hook_repo_get_hook": "2xx", + "gitea_hook_repo_list_git_hooks": "2xx", + "gitea_hook_repo_list_hooks": "2xx", + "gitea_hook_repo_test_hook": "2xx", + "gitea_hook_user_create_hook": "2xx", + "gitea_hook_user_delete_hook": "2xx", + "gitea_hook_user_edit_hook": "2xx", + "gitea_hook_user_get_hook": "2xx", + "gitea_hook_user_list_hooks": "2xx", + "gitea_issue_add_assignees": "2xx", + "gitea_issue_add_label": "2xx", + "gitea_issue_add_subscription": "2xx", + "gitea_issue_add_time": "2xx", + "gitea_issue_check_assignee": "2xx", + "gitea_issue_check_subscription": "2xx", + "gitea_issue_clear_labels": "2xx", + "gitea_issue_create_comment": "2xx", + "gitea_issue_create_issue": "2xx", + "gitea_issue_create_issue_attachment": "2xx", + "gitea_issue_create_issue_blocking": "2xx", + "gitea_issue_create_issue_comment_attachment": "2xx", + "gitea_issue_create_issue_dependencies": "2xx", + "gitea_issue_create_label": "2xx", + "gitea_issue_create_milestone": "2xx", + "gitea_issue_delete": "2xx", + "gitea_issue_delete_comment": "2xx", + "gitea_issue_delete_comment_deprecated": "2xx", + "gitea_issue_delete_comment_reaction": "2xx", + "gitea_issue_delete_issue_attachment": "2xx", + "gitea_issue_delete_issue_comment_attachment": "2xx", + "gitea_issue_delete_issue_reaction": "2xx", + "gitea_issue_delete_label": "2xx", + "gitea_issue_delete_milestone": "2xx", + "gitea_issue_delete_stopwatch": "2xx", + "gitea_issue_delete_subscription": "2xx", + "gitea_issue_delete_time": "2xx", + "gitea_issue_edit_comment": "2xx", + "gitea_issue_edit_comment_deprecated": "2xx", + "gitea_issue_edit_issue": "2xx", + "gitea_issue_edit_issue_attachment": "2xx", + "gitea_issue_edit_issue_comment_attachment": "2xx", + "gitea_issue_edit_issue_deadline": "2xx", + "gitea_issue_edit_label": "2xx", + "gitea_issue_edit_milestone": "2xx", + "gitea_issue_get_comment": "2xx", + "gitea_issue_get_comment_reactions": "2xx", + "gitea_issue_get_comments": "2xx", + "gitea_issue_get_comments_and_timeline": "2xx", + "gitea_issue_get_issue": "2xx", + "gitea_issue_get_issue_attachment": "2xx", + "gitea_issue_get_issue_comment_attachment": "2xx", + "gitea_issue_get_issue_reactions": "2xx", + "gitea_issue_get_label": "2xx", + "gitea_issue_get_labels": "2xx", + "gitea_issue_get_milestone": "2xx", + "gitea_issue_get_milestones_list": "2xx", + "gitea_issue_get_repo_comments": "2xx", + "gitea_issue_list_blocks": "2xx", + "gitea_issue_list_issue_attachments": "2xx", + "gitea_issue_list_issue_comment_attachments": "2xx", + "gitea_issue_list_issue_dependencies": "2xx", + "gitea_issue_list_issues": "2xx", + "gitea_issue_list_labels": "2xx", + "gitea_issue_list_pinned_issues": "2xx", + "gitea_issue_lock_issue": "2xx", + "gitea_issue_move_issue_pin": "2xx", + "gitea_issue_pin_issue": "2xx", + "gitea_issue_post_comment_reaction": "2xx", + "gitea_issue_post_issue_reaction": "2xx", + "gitea_issue_remove_assignees": "2xx", + "gitea_issue_remove_issue_blocking": "2xx", + "gitea_issue_remove_issue_dependencies": "2xx", + "gitea_issue_remove_label": "2xx", + "gitea_issue_replace_labels": "2xx", + "gitea_issue_reset_time": "2xx", + "gitea_issue_search_issues": "2xx", + "gitea_issue_start_stopwatch": "2xx", + "gitea_issue_stop_stopwatch": "2xx", + "gitea_issue_subscriptions": "2xx", + "gitea_issue_tracked_times": "2xx", + "gitea_issue_unlock_issue": "2xx", + "gitea_issue_unpin_issue": "2xx", + "gitea_misc_get_current_token": "2xx", + "gitea_misc_get_general_api_settings": "2xx", + "gitea_misc_get_general_attachment_settings": "2xx", + "gitea_misc_get_general_repository_settings": "2xx", + "gitea_misc_get_general_ui_settings": "2xx", + "gitea_misc_get_gitignore_template_info": "2xx", + "gitea_misc_get_label_template_info": "2xx", + "gitea_misc_get_license_template_info": "2xx", + "gitea_misc_get_signing_key": 404, + "gitea_misc_get_signing_key_ssh": 404, + "gitea_misc_get_version": "2xx", + "gitea_misc_list_gitignores_templates": "2xx", + "gitea_misc_list_label_templates": "2xx", + "gitea_misc_list_license_templates": "2xx", + "gitea_misc_render_markdown": "2xx", + "gitea_misc_render_markdown_raw": "2xx", + "gitea_misc_render_markup": "2xx", + "gitea_misc_topic_search": "2xx", + "gitea_notification_get_list": "2xx", + "gitea_notification_get_repo_list": "2xx", + "gitea_notification_get_thread": "2xx", + "gitea_notification_new_available": "2xx", + "gitea_notification_read_list": "2xx", + "gitea_notification_read_repo_list": "2xx", + "gitea_notification_read_thread": "2xx", + "gitea_org_add_issue_to_project_column": "2xx", + "gitea_org_block_user": "2xx", + "gitea_org_check_user_block": "2xx", + "gitea_org_conceal_member": "2xx", + "gitea_org_create": "2xx", + "gitea_org_create_label": "2xx", + "gitea_org_create_org_repo": "2xx", + "gitea_org_create_org_repo_deprecated": "2xx", + "gitea_org_create_project": "2xx", + "gitea_org_create_project_column": "2xx", + "gitea_org_delete": "2xx", + "gitea_org_delete_avatar": "2xx", + "gitea_org_delete_label": "2xx", + "gitea_org_delete_member": "2xx", + "gitea_org_delete_project": "2xx", + "gitea_org_delete_project_column": "2xx", + "gitea_org_delete_repos": "2xx", + "gitea_org_edit": "2xx", + "gitea_org_edit_label": "2xx", + "gitea_org_edit_project": "2xx", + "gitea_org_edit_project_column": "2xx", + "gitea_org_get": "2xx", + "gitea_org_get_all": "2xx", + "gitea_org_get_label": "2xx", + "gitea_org_get_project": "2xx", + "gitea_org_get_project_column": "2xx", + "gitea_org_is_member": "2xx", + "gitea_org_is_public_member": "2xx", + "gitea_org_list_activity_feeds": "2xx", + "gitea_org_list_blocks": "2xx", + "gitea_org_list_labels": "2xx", + "gitea_org_list_members": "2xx", + "gitea_org_list_project_column_issues": "2xx", + "gitea_org_list_project_columns": "2xx", + "gitea_org_list_projects": "2xx", + "gitea_org_list_public_members": "2xx", + "gitea_org_list_repos": "2xx", + "gitea_org_move_project_columns": "2xx", + "gitea_org_move_project_issue": "2xx", + "gitea_org_publicize_member": "2xx", + "gitea_org_remove_issue_from_project_column": "2xx", + "gitea_org_rename": "2xx", + "gitea_org_set_default_project_column": "2xx", + "gitea_org_unblock_user": "2xx", + "gitea_org_update_avatar": "2xx", + "gitea_package_delete_package": "2xx", + "gitea_package_delete_package_version": "2xx", + "gitea_package_get_latest_package_version": "2xx", + "gitea_package_get_package": "2xx", + "gitea_package_link_package": "2xx", + "gitea_package_list_package_files": "2xx", + "gitea_package_list_package_versions": "2xx", + "gitea_package_list_packages": "2xx", + "gitea_package_unlink_package": "2xx", + "gitea_pull_cancel_scheduled_auto_merge": 404, + "gitea_pull_create_pull_request": "2xx", + "gitea_pull_create_pull_review": "2xx", + "gitea_pull_create_pull_review_comment_reply": "2xx", + "gitea_pull_create_pull_review_requests": "2xx", + "gitea_pull_delete_pull_review": "2xx", + "gitea_pull_delete_pull_review_requests": "2xx", + "gitea_pull_dismiss_pull_review": "2xx", + "gitea_pull_download_pull_diff_or_patch": "2xx", + "gitea_pull_edit_pull_request": "2xx", + "gitea_pull_get_pull_request": "2xx", + "gitea_pull_get_pull_request_by_base_head": "2xx", + "gitea_pull_get_pull_request_commits": "2xx", + "gitea_pull_get_pull_request_files": "2xx", + "gitea_pull_get_pull_review": "2xx", + "gitea_pull_get_pull_review_comments": "2xx", + "gitea_pull_list_pinned_pull_requests": "2xx", + "gitea_pull_list_pull_requests": "2xx", + "gitea_pull_list_pull_reviews": "2xx", + "gitea_pull_merge_pull_request": "2xx", + "gitea_pull_pull_request_is_merged": 404, + "gitea_pull_resolve_pull_review_comment": "2xx", + "gitea_pull_submit_pull_review": "2xx", + "gitea_pull_undismiss_pull_review": "2xx", + "gitea_pull_unresolve_pull_review_comment": "2xx", + "gitea_pull_update_pull_request": "2xx", + "gitea_release_create_release": "2xx", + "gitea_release_create_release_attachment": "2xx", + "gitea_release_delete_release": "2xx", + "gitea_release_delete_release_attachment": "2xx", + "gitea_release_delete_release_by_tag": "2xx", + "gitea_release_edit_release": "2xx", + "gitea_release_edit_release_attachment": "2xx", + "gitea_release_get_latest_release": "2xx", + "gitea_release_get_release": "2xx", + "gitea_release_get_release_attachment": "2xx", + "gitea_release_get_release_by_tag": "2xx", + "gitea_release_list_release_attachments": "2xx", + "gitea_release_list_releases": "2xx", + "gitea_repo_accept_repo_transfer": 404, + "gitea_repo_add_collaborator": "2xx", + "gitea_repo_add_issue_to_project_column": "2xx", + "gitea_repo_add_push_mirror": "2xx", + "gitea_repo_add_team": "2xx", + "gitea_repo_add_topic": "2xx", + "gitea_repo_apply_diff_patch": "2xx", + "gitea_repo_change_files": "2xx", + "gitea_repo_check_assignee": "2xx", + "gitea_repo_check_collaborator": "2xx", + "gitea_repo_check_subscription": "2xx", + "gitea_repo_check_team": "2xx", + "gitea_repo_compare_diff": "2xx", + "gitea_repo_create_branch": "2xx", + "gitea_repo_create_branch_protection": "2xx", + "gitea_repo_create_deploy_token": "2xx", + "gitea_repo_create_file": "2xx", + "gitea_repo_create_fork": "2xx", + "gitea_repo_create_key": "2xx", + "gitea_repo_create_project": "2xx", + "gitea_repo_create_project_column": "2xx", + "gitea_repo_create_status": "2xx", + "gitea_repo_create_tag": "2xx", + "gitea_repo_create_tag_protection": "2xx", + "gitea_repo_delete": "2xx", + "gitea_repo_delete_avatar": "2xx", + "gitea_repo_delete_branch": "2xx", + "gitea_repo_delete_branch_protection": "2xx", + "gitea_repo_delete_collaborator": "2xx", + "gitea_repo_delete_file": "2xx", + "gitea_repo_delete_key": "2xx", + "gitea_repo_delete_project": "2xx", + "gitea_repo_delete_project_column": "2xx", + "gitea_repo_delete_push_mirror": "2xx", + "gitea_repo_delete_subscription": "2xx", + "gitea_repo_delete_tag": "2xx", + "gitea_repo_delete_tag_protection": "2xx", + "gitea_repo_delete_team": "2xx", + "gitea_repo_delete_topic": "2xx", + "gitea_repo_download_commit_diff_or_patch": "2xx", + "gitea_repo_edit": "2xx", + "gitea_repo_edit_branch_protection": "2xx", + "gitea_repo_edit_project": "2xx", + "gitea_repo_edit_project_column": "2xx", + "gitea_repo_edit_tag_protection": "2xx", + "gitea_repo_generate_repo": "2xx", + "gitea_repo_get": "2xx", + "gitea_repo_get_all_commits": "2xx", + "gitea_repo_get_annotated_tag": "2xx", + "gitea_repo_get_archive": "2xx", + "gitea_repo_get_assignees": "2xx", + "gitea_repo_get_blob": "2xx", + "gitea_repo_get_branch": "2xx", + "gitea_repo_get_branch_protection": "2xx", + "gitea_repo_get_by_id": "2xx", + "gitea_repo_get_combined_status_by_ref": "2xx", + "gitea_repo_get_commit_pull_request": 404, + "gitea_repo_get_contents": "2xx", + "gitea_repo_get_contents_ext": "2xx", + "gitea_repo_get_contents_list": "2xx", + "gitea_repo_get_editor_config": "2xx", + "gitea_repo_get_file_contents": "2xx", + "gitea_repo_get_file_contents_post": "2xx", + "gitea_repo_get_issue_config": "2xx", + "gitea_repo_get_issue_templates": "2xx", + "gitea_repo_get_key": "2xx", + "gitea_repo_get_languages": "2xx", + "gitea_repo_get_licenses": "2xx", + "gitea_repo_get_note": 404, + "gitea_repo_get_project": "2xx", + "gitea_repo_get_project_column": "2xx", + "gitea_repo_get_push_mirror_by_remote_name": "2xx", + "gitea_repo_get_raw_file": "2xx", + "gitea_repo_get_raw_file_or_lfs": "2xx", + "gitea_repo_get_repo_permissions": "2xx", + "gitea_repo_get_reviewers": "2xx", + "gitea_repo_get_single_commit": "2xx", + "gitea_repo_get_tag": "2xx", + "gitea_repo_get_tag_protection": "2xx", + "gitea_repo_get_tree": "2xx", + "gitea_repo_list_activity_feeds": "2xx", + "gitea_repo_list_all_git_refs": "2xx", + "gitea_repo_list_branch_protection": "2xx", + "gitea_repo_list_branches": "2xx", + "gitea_repo_list_collaborators": "2xx", + "gitea_repo_list_forks": "2xx", + "gitea_repo_list_git_refs": "2xx", + "gitea_repo_list_keys": "2xx", + "gitea_repo_list_project_column_issues": "2xx", + "gitea_repo_list_project_columns": "2xx", + "gitea_repo_list_projects": "2xx", + "gitea_repo_list_push_mirrors": "2xx", + "gitea_repo_list_stargazers": "2xx", + "gitea_repo_list_statuses": "2xx", + "gitea_repo_list_statuses_by_ref": "2xx", + "gitea_repo_list_subscribers": "2xx", + "gitea_repo_list_tag_protection": "2xx", + "gitea_repo_list_tags": "2xx", + "gitea_repo_list_teams": "2xx", + "gitea_repo_list_topics": "2xx", + "gitea_repo_list_user_tracked_times": "2xx", + "gitea_repo_merge_upstream": "2xx", + "gitea_repo_migrate": "2xx", + "gitea_repo_mirror_sync": "2xx", + "gitea_repo_move_project_columns": "2xx", + "gitea_repo_move_project_issue": "2xx", + "gitea_repo_new_pin_allowed": "2xx", + "gitea_repo_push_mirror_sync": "2xx", + "gitea_repo_put_subscription": "2xx", + "gitea_repo_reject_repo_transfer": 404, + "gitea_repo_remove_issue_from_project_column": "2xx", + "gitea_repo_rename_branch": "2xx", + "gitea_repo_search": "2xx", + "gitea_repo_set_default_project_column": "2xx", + "gitea_repo_signing_key": 404, + "gitea_repo_signing_key_ssh": 404, + "gitea_repo_tracked_times": "2xx", + "gitea_repo_transfer": "2xx", + "gitea_repo_update_avatar": "2xx", + "gitea_repo_update_branch": "2xx", + "gitea_repo_update_branch_protection_priorities": "2xx", + "gitea_repo_update_file": "2xx", + "gitea_repo_update_topics": "2xx", + "gitea_repo_validate_issue_config": "2xx", + "gitea_team_add_team_member": "2xx", + "gitea_team_add_team_repository": "2xx", + "gitea_team_create_team": "2xx", + "gitea_team_delete_team": "2xx", + "gitea_team_edit_team": "2xx", + "gitea_team_get_team": "2xx", + "gitea_team_list_team_activity_feeds": "2xx", + "gitea_team_list_team_member": "2xx", + "gitea_team_list_team_members": "2xx", + "gitea_team_list_team_repo": "2xx", + "gitea_team_list_team_repos": "2xx", + "gitea_team_list_teams": "2xx", + "gitea_team_remove_team_member": "2xx", + "gitea_team_remove_team_repository": "2xx", + "gitea_team_search": "2xx", + "gitea_user_add_email": "2xx", + "gitea_user_block_user": "2xx", + "gitea_user_check_following": "2xx", + "gitea_user_check_user_block": "2xx", + "gitea_user_create_oauth2_application": "2xx", + "gitea_user_create_token": 401, + "gitea_user_current_add_issue_to_project_column": "2xx", + "gitea_user_current_check_following": "2xx", + "gitea_user_current_check_starring": "2xx", + "gitea_user_current_create_project": "2xx", + "gitea_user_current_create_project_column": "2xx", + "gitea_user_current_create_repo": "2xx", + "gitea_user_current_delete_follow": "2xx", + "gitea_user_current_delete_gpg_key": "2xx", + "gitea_user_current_delete_key": "2xx", + "gitea_user_current_delete_project": "2xx", + "gitea_user_current_delete_project_column": "2xx", + "gitea_user_current_delete_star": "2xx", + "gitea_user_current_edit_project": "2xx", + "gitea_user_current_edit_project_column": "2xx", + "gitea_user_current_get_gpg_key": "2xx", + "gitea_user_current_get_key": "2xx", + "gitea_user_current_get_project": "2xx", + "gitea_user_current_get_project_column": "2xx", + "gitea_user_current_list_followers": "2xx", + "gitea_user_current_list_following": "2xx", + "gitea_user_current_list_gpg_keys": "2xx", + "gitea_user_current_list_keys": "2xx", + "gitea_user_current_list_orgs": "2xx", + "gitea_user_current_list_project_column_issues": "2xx", + "gitea_user_current_list_project_columns": "2xx", + "gitea_user_current_list_projects": "2xx", + "gitea_user_current_list_repos": "2xx", + "gitea_user_current_list_starred": "2xx", + "gitea_user_current_list_subscriptions": "2xx", + "gitea_user_current_move_project_columns": "2xx", + "gitea_user_current_move_project_issue": "2xx", + "gitea_user_current_post_gpg_key": "2xx", + "gitea_user_current_post_key": "2xx", + "gitea_user_current_put_follow": "2xx", + "gitea_user_current_put_star": "2xx", + "gitea_user_current_remove_issue_from_project_column": "2xx", + "gitea_user_current_set_default_project_column": "2xx", + "gitea_user_current_tracked_times": "2xx", + "gitea_user_delete_access_token": 401, + "gitea_user_delete_avatar": "2xx", + "gitea_user_delete_email": "2xx", + "gitea_user_delete_oauth2_application": "2xx", + "gitea_user_get": "2xx", + "gitea_user_get_current": "2xx", + "gitea_user_get_heatmap_data": "2xx", + "gitea_user_get_oauth2_application": "2xx", + "gitea_user_get_org_permissions": "2xx", + "gitea_user_get_settings": "2xx", + "gitea_user_get_stopwatches": "2xx", + "gitea_user_get_tokens": 401, + "gitea_user_get_verification_token": "2xx", + "gitea_user_list_activity_feeds": "2xx", + "gitea_user_list_blocks": "2xx", + "gitea_user_list_emails": "2xx", + "gitea_user_list_followers": "2xx", + "gitea_user_list_following": "2xx", + "gitea_user_list_gpg_keys": "2xx", + "gitea_user_list_keys": "2xx", + "gitea_user_list_oauth2_applications": "2xx", + "gitea_user_list_orgs": "2xx", + "gitea_user_list_projects": "2xx", + "gitea_user_list_repos": "2xx", + "gitea_user_list_starred": "2xx", + "gitea_user_list_subscriptions": "2xx", + "gitea_user_list_teams": "2xx", + "gitea_user_search": "2xx", + "gitea_user_unblock_user": "2xx", + "gitea_user_update_avatar": "2xx", + "gitea_user_update_oauth2_application": "2xx", + "gitea_user_update_settings": "2xx", + "gitea_user_verify_gpg_key": 422, + "gitea_wiki_create_wiki_page": "2xx", + "gitea_wiki_delete_wiki_page": "2xx", + "gitea_wiki_edit_wiki_page": "2xx", + "gitea_wiki_get_wiki_page": "2xx", + "gitea_wiki_get_wiki_page_revisions": "2xx", + "gitea_wiki_get_wiki_pages": "2xx" +} +} diff --git a/tests/integration/sweep_plan.py b/tests/integration/sweep_plan.py index 77513a3..4f7f38f 100644 --- a/tests/integration/sweep_plan.py +++ b/tests/integration/sweep_plan.py @@ -12,6 +12,9 @@ that is data or pure logic, so it can be checked without Docker then deletes (leaf resources before their parents). * :func:`classify` decides whether a result is acceptable: a 2xx, a status the swagger documents for that operation, or an explicit entry of :data:`ALLOWANCES` with a reason. +* :func:`compare_baseline` pins the outcome class of every tool (``2xx`` or the exact + status of a documented non-2xx or an allowance) in ``sweep_baseline.json`` and reports + every downgrade at once; ``AEGIS_IT_WRITE_BASELINE=1`` regenerates the file. * :func:`render_report` writes ``docs/integration-coverage.md``. """ @@ -27,12 +30,15 @@ from pathlib import Path from typing import Any from aegis_gitea_mcp.catalog import Catalog, Operation +from tests.integration.static_keys import GPG_PUBLIC_KEYS from tests.integration.sweep_seed import PNG_1X1, Ctx, b64, ssh_public_key REPO_ROOT = Path(__file__).resolve().parents[2] SPEC_PATH = REPO_ROOT / "spec" / "gitea-swagger.v1.json" REPORT_PATH = REPO_ROOT / "docs" / "integration-coverage.md" REPORT_ENV = "AEGIS_IT_WRITE_REPORT" +BASELINE_PATH = Path(__file__).resolve().parent / "sweep_baseline.json" +BASELINE_ENV = "AEGIS_IT_WRITE_BASELINE" RAW_TOOL = "gitea_api_request" #: Tools the generic sweep never calls, with the test that covers them in isolation. @@ -172,6 +178,7 @@ ID_RULES: list[tuple[str, str]] = [ (r"^/orgs/\{org\}/projects/\{id\}", "org_project"), (r"^/user/hooks/\{id\}", "user_hook"), (r"^/user/keys/\{id\}", "user_key"), + (r"^/user/gpg_keys/\{id\}", "gpg_key"), (r"^/user/projects/\{id\}", "user_project"), (r"^/user/applications/oauth2/\{id\}", "oauth_app"), (r"^/admin/hooks/\{id\}", "admin_hook"), @@ -605,7 +612,9 @@ OVERRIDES: dict[str, Override] = { "gitea_user_current_post_key": lambda c: { "body": {"title": fresh(c, "key"), "key": ssh_public_key()} }, - "gitea_user_current_post_gpg_key": lambda c: {"body": {"armored_public_key": "not a key"}}, + "gitea_user_current_post_gpg_key": lambda c: { + "body": {"armored_public_key": GPG_PUBLIC_KEYS[1]} + }, "gitea_user_delete_access_token": lambda c: { "username": c["admin"], "token": c["throwaway_token"], @@ -858,6 +867,88 @@ def write_report_if_enabled(catalog: Catalog, gitea_version: str) -> bool: return True +# --------------------------------------------------------------------------- +# Baseline: the pinned outcome class per tool +# --------------------------------------------------------------------------- + +SUCCESS = "2xx" +NO_STATUS = "no-status" +Outcome = str | int + + +def outcome_class(record: SweepRecord) -> Outcome: + """``"2xx"`` for a success, else the HTTP status Gitea answered (or ``"no-status"``).""" + if record.status is not None and 200 <= record.status < 300 and not record.is_error: + return SUCCESS + return record.status if record.status is not None else NO_STATUS + + +def baseline_snapshot(records: Iterable[SweepRecord], gitea_version: str) -> dict[str, Any]: + """The baseline document for ``records`` (what ``AEGIS_IT_WRITE_BASELINE=1`` writes).""" + return { + "gitea": gitea_version, + "comment": ( + "Expected outcome class per tool in the sweep: 2xx, or the exact status of a " + "documented non-2xx or allowance. Regenerate with AEGIS_IT_WRITE_BASELINE=1." + ), + "tools": {record.tool: outcome_class(record) for record in records}, + } + + +def load_baseline(path: Path = BASELINE_PATH) -> dict[str, Outcome]: + """Read the pinned outcome per tool.""" + data = json.loads(path.read_text(encoding="utf-8")) + tools: dict[str, Outcome] = data["tools"] + return tools + + +def write_baseline( + records: Iterable[SweepRecord], gitea_version: str, path: Path = BASELINE_PATH +) -> None: + """Write the baseline file (tools sorted by name, so diffs show only real changes).""" + document = baseline_snapshot(records, gitea_version) + document["tools"] = dict(sorted(document["tools"].items())) + path.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8") + + +@dataclass +class BaselineDiff: + """Result of comparing a sweep with the pinned baseline.""" + + regressions: list[str] = field(default_factory=list) + upgrades: list[str] = field(default_factory=list) + + +def compare_baseline(records: Iterable[SweepRecord], baseline: dict[str, Outcome]) -> BaselineDiff: + """Compare outcomes with the baseline. + + A regression is a 2xx that is no longer a 2xx, a documented non-2xx that answers a + different status, a tool without a baseline entry or a baseline entry without a tool. + A non-2xx that became a 2xx is an upgrade: reported, but it does not fail the run. + """ + diff = BaselineDiff() + seen: set[str] = set() + for record in records: + seen.add(record.tool) + actual = outcome_class(record) + if record.tool not in baseline: + diff.regressions.append(f"{record.tool}: no baseline entry (got {actual})") + continue + expected = baseline[record.tool] + if actual == expected: + continue + if actual == SUCCESS: + diff.upgrades.append(f"{record.tool}: baseline {expected}, now 2xx") + continue + detail = f"{record.tool}: baseline {expected}, got {actual} ({record.method} {record.path}" + if record.message: + detail += f" :: {record.message}" + diff.regressions.append(detail + ")") + for tool in sorted(set(baseline) - seen): + diff.regressions.append(f"{tool}: in the baseline but not called by the sweep") + return diff + + def failing(records: Iterable[SweepRecord]) -> list[SweepRecord]: """The records whose outcome is ``unexpected``.""" return [record for record in records if record.outcome == "unexpected"] diff --git a/tests/integration/sweep_seed.py b/tests/integration/sweep_seed.py index d311204..cd9ed39 100644 --- a/tests/integration/sweep_seed.py +++ b/tests/integration/sweep_seed.py @@ -16,6 +16,7 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from tests.integration.harness import GiteaInstance, HarnessError +from tests.integration.static_keys import GPG_EMAILS, GPG_PUBLIC_KEYS Ctx = dict[str, Any] @@ -370,7 +371,11 @@ def seed(gitea: GiteaInstance, user: str, user_token: str) -> Ctx: # Current user: email, follow, star, watch, block. c["email"] = f"sweep-{sfx}@example.test" - s.call("POST", "/user/emails", json={"emails": [c["email"]]}) + s.call("POST", "/user/emails", json={"emails": [c["email"], *GPG_EMAILS]}) + # Gitea only accepts a GPG key whose identity matches an email of the account. + c["gpg_key"] = s.call( + "POST", "/user/gpg_keys", json={"armored_public_key": GPG_PUBLIC_KEYS[0]} + )["id"] s.call("PUT", f"/user/following/{user}") s.call("PUT", f"/user/starred/{admin}/{repo}") s.call("PUT", f"{base}/subscription") @@ -419,8 +424,12 @@ def _project(s: Seeder, c: Ctx, scope: str, path: str) -> None: def _find_run(s: Seeder, base: str) -> tuple[Any, Any]: - """Return ``(run id, job id)`` of the first workflow run, or ``(1, 1)`` if none appeared.""" - deadline = time.monotonic() + 10 + """Return ``(run id, job id)`` of the first workflow run; fail when none appears. + + A made-up id would only turn every dependent tool into a 404, which the sweep would + accept as documented, so a missing run is a seeding failure. + """ + deadline = time.monotonic() + 30 while time.monotonic() < deadline: runs = s.call("GET", f"{base}/actions/runs") entries = runs.get("workflow_runs") or [] @@ -431,15 +440,15 @@ def _find_run(s: Seeder, base: str) -> tuple[Any, Any]: if job_entries: return run_id, job_entries[0]["id"] time.sleep(0.5) - return 1, 1 + raise HarnessError("seed: the push of the workflow file did not create a workflow run") def _find_notification(s: Seeder, admin: str) -> Any: - """Return the id of a notification of the admin if one exists, else 1.""" - deadline = time.monotonic() + 5 + """Return the id of a notification of the admin; fail when none appears.""" + deadline = time.monotonic() + 30 while time.monotonic() < deadline: listing = s.call("GET", "/notifications", params={"all": "true"}) if listing: return listing[0]["id"] time.sleep(0.5) - return 1 + raise HarnessError(f"seed: no notification arrived for {admin}") diff --git a/tests/integration/test_sweep.py b/tests/integration/test_sweep.py index 3254317..0a845d2 100644 --- a/tests/integration/test_sweep.py +++ b/tests/integration/test_sweep.py @@ -2,6 +2,8 @@ from __future__ import annotations +import os + import pytest from aegis_gitea_mcp.audit import validate_audit_log_integrity @@ -9,20 +11,20 @@ from aegis_gitea_mcp.catalog import load_catalog from tests.integration.conftest import McpSession, RegularUser from tests.integration.harness import GiteaInstance from tests.integration.sweep_plan import ( + BASELINE_ENV, EXCLUDED, LOG, RAW_TOOL, + compare_baseline, failing, + load_baseline, load_spec, run_sweep, summarize, + write_baseline, ) from tests.integration.sweep_seed import Ctx, seed -#: At least this share of the tools must get a real 2xx, so a systematic argument problem -#: cannot hide behind documented 404 responses. -MIN_SUCCESS_SHARE = 0.55 - @pytest.fixture(scope="module") def sweep_context(gitea: GiteaInstance, regular_user: RegularUser) -> Ctx: @@ -53,8 +55,23 @@ async def test_every_tool_reaches_gitea( for r in bad ) assert not bad, f"{len(bad)} tool(s) with an unexpected result ({summary}):\n{report}" - success_share = summary.get("success", 0) / len(records) - assert success_share >= MIN_SUCCESS_SHARE, f"only {success_share:.0%} of tools got a 2xx" + + # The pinned outcome per tool: an argument or path regression cannot hide behind a + # documented 404, because a 2xx that turns into any other status fails here. + if os.environ.get(BASELINE_ENV) == "1": + write_baseline(records, load_catalog().spec_version) + else: + diff = compare_baseline(records, load_baseline()) + if diff.upgrades: + print( + f"\n{len(diff.upgrades)} tool(s) improved on the baseline; refresh it with " + f"{BASELINE_ENV}=1:\n " + "\n ".join(diff.upgrades) + ) + assert not diff.regressions, ( + f"{len(diff.regressions)} tool(s) differ from sweep_baseline.json " + f"(regenerate with {BASELINE_ENV}=1 only when the change is intended):\n " + + "\n ".join(diff.regressions) + ) # Whatever the 538 calls did, no credential or secret value came back in a result. hidden = [ diff --git a/tests/test_sweep_plan.py b/tests/test_sweep_plan.py index 9a4d97b..01dfd05 100644 --- a/tests/test_sweep_plan.py +++ b/tests/test_sweep_plan.py @@ -271,3 +271,49 @@ def test_report_is_only_written_when_enabled( ] assert sweep_plan.write_report_if_enabled(CATALOG, "28.1.0") is True assert target.read_text(encoding="utf-8").startswith("# Integration coverage") + + +def _record(tool: str, status: int | None, is_error: bool = False) -> SweepRecord: + return SweepRecord(tool, "repo", "GET", "/x", status, is_error, None, "", "success") + + +def test_outcome_class_is_2xx_or_the_exact_status() -> None: + assert sweep_plan.outcome_class(_record("t", 200)) == "2xx" + assert sweep_plan.outcome_class(_record("t", 204)) == "2xx" + assert sweep_plan.outcome_class(_record("t", 404, True)) == 404 + assert sweep_plan.outcome_class(_record("t", 200, True)) == 200 # an error flag wins + assert sweep_plan.outcome_class(_record("t", None, True)) == "no-status" + + +def test_baseline_reports_every_regression_at_once_and_notes_upgrades() -> None: + baseline: dict[str, Any] = {"a": "2xx", "b": 404, "c": 404, "d": "2xx", "gone": "2xx"} + records = [ + _record("a", 404, True), # 2xx -> 404: regression + _record("b", 200), # 404 -> 2xx: upgrade + _record("c", 403, True), # 404 -> 403: a different status is a regression + _record("d", 200), # unchanged + _record("new", 200), # not in the baseline + ] + + diff = sweep_plan.compare_baseline(records, baseline) + + assert diff.upgrades == ["b: baseline 404, now 2xx"] + assert len(diff.regressions) == 4 + assert diff.regressions[0].startswith("a: baseline 2xx, got 404") + assert diff.regressions[1].startswith("c: baseline 404, got 403") + assert "new: no baseline entry" in diff.regressions[2] + assert diff.regressions[3] == "gone: in the baseline but not called by the sweep" + + +def test_baseline_round_trips_and_covers_the_whole_sweep(tmp_path: Any) -> None: + target = tmp_path / "baseline.json" + records = [_record("b", 404, True), _record("a", 200)] + sweep_plan.write_baseline(records, "28.1.0", target) + assert sweep_plan.load_baseline(target) == {"a": "2xx", "b": 404} + assert list(sweep_plan.load_baseline(target)) == ["a", "b"] + + # The committed baseline names exactly the tools the sweep calls. + committed = sweep_plan.load_baseline() + swept = {op.tool_name for op in ordered_operations(CATALOG)} | {RAW_TOOL} + assert set(committed) == swept + assert all(value == "2xx" or isinstance(value, int) for value in committed.values())