Feature/generic webhook (#1020)
* added api endpoint for a generic webhook w/ burnettk * added the start of test for testing webhooks w/ burnettk * the initial test for webhooks is now working w/ burnettk * added test to prove we can run a message send from a non-persistent process instance w/ burnettk * pyl w/ burnettk * updated connector-http for patch command w/ burnettk * make the webhook persistent so the message instance can be created w/ burnettk * make sure we commit the message instance to the db in the webhook code w/ burnettk --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
73f7ab8ff4
commit
cc3d10f340
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
|
@ -220,7 +220,7 @@ spiffworkflow-connector-command = {git = "https://github.com/sartography/spiffwo
|
|||
type = "git"
|
||||
url = "https://github.com/sartography/connector-http.git"
|
||||
reference = "HEAD"
|
||||
resolved_reference = "026de90d01e1127b7944600818aa94dc53850518"
|
||||
resolved_reference = "5e6a675a421cbee85f9c33832b874152a2a57e1e"
|
||||
|
||||
[[package]]
|
||||
name = "connector-slack"
|
||||
|
|
|
@ -747,7 +747,7 @@ paths:
|
|||
|
||||
/github-webhook-receive:
|
||||
post:
|
||||
operationId: spiffworkflow_backend.routes.process_api_blueprint.github_webhook_receive
|
||||
operationId: spiffworkflow_backend.routes.webhooks_controller.github_webhook_receive
|
||||
summary: receives push webhooks from github so we can keep our process model repo up to date
|
||||
requestBody:
|
||||
content:
|
||||
|
@ -764,6 +764,25 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/webhook:
|
||||
post:
|
||||
operationId: spiffworkflow_backend.routes.webhooks_controller.webhook
|
||||
summary: receives webhooks from external systems and runs a process model using the data received from the caller so arbitrary handling can be achieved.
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
tags:
|
||||
- git
|
||||
responses:
|
||||
"200":
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/process-instances/for-me:
|
||||
parameters:
|
||||
- name: process_model_identifier
|
||||
|
|
|
@ -165,6 +165,15 @@ config_from_env("SPIFFWORKFLOW_BACKEND_GIT_USER_EMAIL")
|
|||
config_from_env("SPIFFWORKFLOW_BACKEND_GITHUB_WEBHOOK_SECRET")
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_GIT_SSH_PRIVATE_KEY_PATH")
|
||||
|
||||
### webhook
|
||||
# configs for handling incoming webhooks from other systems
|
||||
# it assumes github webhooks by default, since SPIFFWORKFLOW_BACKEND_WEBHOOK_ENFORCES_GITHUB_AUTH is true,
|
||||
# but if you set that to false, you can handle webhooks from any system. just make sure you supply your
|
||||
# own auth checks in the process model.
|
||||
# the github auth will use SPIFFWORKFLOW_BACKEND_GITHUB_WEBHOOK_SECRET from above.
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_WEBHOOK_ENFORCES_GITHUB_AUTH", default=True)
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_WEBHOOK_PROCESS_MODEL_IDENTIFIER")
|
||||
|
||||
### element units
|
||||
# disabling until we fix the "no such directory" error so we do not keep sending cypress errors
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_ELEMENT_UNITS_CACHE_DIR", default="src/instance/element-unit-cache")
|
||||
|
|
|
@ -17,6 +17,9 @@ SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS = None
|
|||
SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get("SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="debug")
|
||||
SPIFFWORKFLOW_BACKEND_GIT_COMMIT_ON_SAVE = False
|
||||
|
||||
SPIFFWORKFLOW_BACKEND_WEBHOOK_PROCESS_MODEL_IDENTIFIER = "test_group/simple_script"
|
||||
SPIFFWORKFLOW_BACKEND_GITHUB_WEBHOOK_SECRET = "test_github_webhook_secret" # noqa: S105
|
||||
|
||||
# NOTE: set this here since nox shoves tests and src code to
|
||||
# different places and this allows us to know exactly where we are at the start
|
||||
worker_id = environ.get("PYTEST_XDIST_WORKER")
|
||||
|
|
|
@ -15,6 +15,7 @@ from spiffworkflow_backend.models.group import GroupModel
|
|||
|
||||
SPIFF_NO_AUTH_USER = "spiff_no_auth_guest_user"
|
||||
SPIFF_GUEST_USER = "spiff_guest_user"
|
||||
SPIFF_SYSTEM_USER = "spiff_system_user"
|
||||
SPIFF_GENERATED_JWT_KEY_ID = "spiff_backend"
|
||||
SPIFF_GENERATED_JWT_ALGORITHM = "HS256"
|
||||
SPIFF_GENERATED_JWT_AUDIENCE = "spiffworkflow-backend"
|
||||
|
|
|
@ -7,25 +7,16 @@ from flask import g
|
|||
from flask import jsonify
|
||||
from flask import make_response
|
||||
from flask.wrappers import Response
|
||||
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
|
||||
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import _un_modify_modified_process_model_id
|
||||
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
|
||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||
from spiffworkflow_backend.services.jinja_service import JinjaService
|
||||
from spiffworkflow_backend.services.process_instance_processor import CustomBpmnScriptEngine
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsAlreadyLockedError
|
||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsNotEnqueuedError
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||
from spiffworkflow_backend.services.spec_file_service import SpecFileService
|
||||
from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionServiceError
|
||||
|
||||
|
||||
def extension_run(
|
||||
|
@ -108,56 +99,17 @@ def _run_extension(
|
|||
persistence_level = ui_schema_action.get("persistence_level", "none")
|
||||
process_id_to_run = ui_schema_action.get("process_id_to_run", None)
|
||||
|
||||
process_instance = None
|
||||
if persistence_level == "none":
|
||||
process_instance = ProcessInstanceModel(
|
||||
status=ProcessInstanceStatus.not_started.value,
|
||||
process_initiator_id=g.user.id,
|
||||
process_model_identifier=process_model.id,
|
||||
process_model_display_name=process_model.display_name,
|
||||
persistence_level=persistence_level,
|
||||
)
|
||||
else:
|
||||
process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
|
||||
process_model_identifier, g.user
|
||||
)
|
||||
data_to_inject = None
|
||||
if body and "extension_input" in body:
|
||||
data_to_inject = body["extension_input"]
|
||||
|
||||
processor = None
|
||||
try:
|
||||
# this is only creates new process instances so no need to worry about process instance migrations
|
||||
processor = ProcessInstanceProcessor(
|
||||
process_instance,
|
||||
script_engine=CustomBpmnScriptEngine(use_restricted_script_engine=False),
|
||||
process_id_to_run=process_id_to_run,
|
||||
)
|
||||
save_to_db = process_instance.persistence_level != "none"
|
||||
if body and "extension_input" in body:
|
||||
processor.do_engine_steps(save=save_to_db, execution_strategy_name="run_current_ready_tasks")
|
||||
next_task = processor.next_task()
|
||||
DeepMerge.merge(next_task.data, body["extension_input"])
|
||||
processor.do_engine_steps(save=save_to_db, execution_strategy_name="greedy")
|
||||
except (
|
||||
ApiError,
|
||||
ProcessInstanceIsNotEnqueuedError,
|
||||
ProcessInstanceIsAlreadyLockedError,
|
||||
WorkflowExecutionServiceError,
|
||||
) as e:
|
||||
ErrorHandlingService.handle_error(process_instance, e)
|
||||
raise e
|
||||
except Exception as e:
|
||||
ErrorHandlingService.handle_error(process_instance, e)
|
||||
# FIXME: this is going to point someone to the wrong task - it's misinformation for errors in sub-processes.
|
||||
# we need to recurse through all last tasks if the last task is a call activity or subprocess.
|
||||
if processor is not None:
|
||||
task = processor.bpmn_process_instance.last_task
|
||||
if task is not None:
|
||||
raise ApiError.from_task(
|
||||
error_code="unknown_exception",
|
||||
message=f"An unknown error occurred. Original error: {e}",
|
||||
status_code=400,
|
||||
task=task,
|
||||
) from e
|
||||
raise e
|
||||
processor = ProcessInstanceService.create_and_run_process_instance(
|
||||
process_model=process_model,
|
||||
persistence_level=persistence_level,
|
||||
data_to_inject=data_to_inject,
|
||||
process_id_to_run=process_id_to_run,
|
||||
user=g.user,
|
||||
)
|
||||
|
||||
task_data = {}
|
||||
if processor is not None:
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
|
@ -8,7 +7,6 @@ from flask import current_app
|
|||
from flask import g
|
||||
from flask import jsonify
|
||||
from flask import make_response
|
||||
from flask import request
|
||||
from flask.wrappers import Response
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy import or_
|
||||
|
@ -209,20 +207,6 @@ def process_data_file_download(
|
|||
)
|
||||
|
||||
|
||||
# sample body:
|
||||
# {"ref": "refs/heads/main", "repository": {"name": "sample-process-models",
|
||||
# "full_name": "sartography/sample-process-models", "private": False .... }}
|
||||
# test with: ngrok http 7000
|
||||
# or with:
|
||||
# npm install -g localtunnel && lt --port 7000 --subdomain oh-so-hot
|
||||
# where 7000 is the port the app is running on locally
|
||||
def github_webhook_receive(body: dict) -> Response:
|
||||
auth_header = request.headers.get("X-Hub-Signature-256")
|
||||
AuthenticationService.verify_sha256_token(auth_header)
|
||||
result = GitService.handle_web_hook(body)
|
||||
return Response(json.dumps({"git_pull": result}), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
def _get_required_parameter_or_raise(parameter: str, post_body: dict[str, Any]) -> Any:
|
||||
return_value = None
|
||||
if parameter in post_body:
|
||||
|
@ -357,3 +341,19 @@ def _find_process_instance_for_me_or_raise(
|
|||
process_instance.actions = {"read": {"path": target_uri, "method": "GET"}}
|
||||
|
||||
return process_instance
|
||||
|
||||
|
||||
def _get_process_model_for_instantiation(
|
||||
process_model_identifier: str,
|
||||
) -> ProcessModelInfo:
|
||||
process_model = _get_process_model(process_model_identifier)
|
||||
if process_model.primary_file_name is None:
|
||||
raise ApiError(
|
||||
error_code="process_model_missing_primary_bpmn_file",
|
||||
message=(
|
||||
f"Process Model '{process_model_identifier}' does not have a primary"
|
||||
" bpmn file. One must be set in order to instantiate this model."
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
return process_model
|
||||
|
|
|
@ -40,6 +40,7 @@ from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
|
|||
from spiffworkflow_backend.routes.process_api_blueprint import _find_process_instance_by_id_or_raise
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import _find_process_instance_for_me_or_raise
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model_for_instantiation
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import _un_modify_modified_process_model_id
|
||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
|
||||
|
@ -685,7 +686,7 @@ def _process_instance_run(
|
|||
def _process_instance_create(
|
||||
process_model_identifier: str,
|
||||
) -> ProcessInstanceModel:
|
||||
process_model = _get_process_model(process_model_identifier)
|
||||
process_model = _get_process_model_for_instantiation(process_model_identifier)
|
||||
if process_model.primary_file_name is None:
|
||||
raise ApiError(
|
||||
error_code="process_model_missing_primary_bpmn_file",
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import json
|
||||
|
||||
from flask import current_app
|
||||
from flask import request
|
||||
from flask.wrappers import Response
|
||||
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model_for_instantiation
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import _un_modify_modified_process_model_id
|
||||
from spiffworkflow_backend.services.authentication_service import AuthenticationService # noqa: F401
|
||||
from spiffworkflow_backend.services.git_service import GitService
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
|
||||
|
||||
# sample body:
|
||||
# {"ref": "refs/heads/main", "repository": {"name": "sample-process-models",
|
||||
# "full_name": "sartography/sample-process-models", "private": False .... }}
|
||||
# test with: ngrok http 7000
|
||||
# or with:
|
||||
# npm install -g localtunnel && lt --port 7000 --subdomain oh-so-hot
|
||||
# where 7000 is the port the app is running on locally
|
||||
# so this would work: curl https://oh-so-hot.loca.lt/v1.0/status
|
||||
def github_webhook_receive(body: dict) -> Response:
|
||||
_enforce_github_auth()
|
||||
result = GitService.handle_web_hook(body)
|
||||
return Response(json.dumps({"git_pull": result}), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
def webhook(body: dict) -> Response:
|
||||
if current_app.config["SPIFFWORKFLOW_BACKEND_WEBHOOK_ENFORCES_GITHUB_AUTH"] is True:
|
||||
_enforce_github_auth()
|
||||
|
||||
if current_app.config["SPIFFWORKFLOW_BACKEND_WEBHOOK_PROCESS_MODEL_IDENTIFIER"] is None:
|
||||
error_message = "Webhook process model implementation not configured"
|
||||
raise ApiError(
|
||||
error_code="webhook_not_configured",
|
||||
message=error_message,
|
||||
status_code=501,
|
||||
)
|
||||
|
||||
process_model = _get_process_model_for_instantiation(
|
||||
_un_modify_modified_process_model_id(current_app.config["SPIFFWORKFLOW_BACKEND_WEBHOOK_PROCESS_MODEL_IDENTIFIER"])
|
||||
)
|
||||
ProcessInstanceService.create_and_run_process_instance(
|
||||
process_model=process_model,
|
||||
persistence_level="none",
|
||||
data_to_inject={"headers": dict(request.headers), "body": body},
|
||||
)
|
||||
|
||||
# ensure we commit the message instances
|
||||
db.session.commit()
|
||||
|
||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
def _enforce_github_auth() -> None:
|
||||
auth_header = request.headers.get("X-Hub-Signature-256")
|
||||
AuthenticationService.verify_sha256_token(auth_header)
|
|
@ -241,6 +241,7 @@ class AuthorizationService:
|
|||
"task_allows_guest",
|
||||
"test_raise_error",
|
||||
"url_info",
|
||||
"webhook",
|
||||
]
|
||||
if request.method == "OPTIONS":
|
||||
return True
|
||||
|
|
|
@ -13,6 +13,7 @@ from spiffworkflow_backend.models.user import UserModel
|
|||
from spiffworkflow_backend.services.process_instance_processor import CustomBpmnScriptEngine
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
from spiffworkflow_backend.services.user_service import UserService
|
||||
|
||||
|
||||
class MessageServiceError(Exception):
|
||||
|
@ -52,9 +53,10 @@ class MessageService:
|
|||
message_name=message_instance_send.name
|
||||
).first()
|
||||
if message_triggerable_process_model:
|
||||
receiving_process = MessageService.start_process_with_message(
|
||||
message_triggerable_process_model, message_instance_send.user
|
||||
)
|
||||
user: UserModel | None = message_instance_send.user
|
||||
if user is None:
|
||||
user = UserService.find_or_create_system_user()
|
||||
receiving_process = MessageService.start_process_with_message(message_triggerable_process_model, user)
|
||||
message_instance_receive = MessageInstanceModel.query.filter_by(
|
||||
process_instance_id=receiving_process.id,
|
||||
message_type="receive",
|
||||
|
|
|
@ -45,13 +45,17 @@ from spiffworkflow_backend.models.task import Task
|
|||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
|
||||
from spiffworkflow_backend.services.git_service import GitCommandError
|
||||
from spiffworkflow_backend.services.git_service import GitService
|
||||
from spiffworkflow_backend.services.process_instance_processor import CustomBpmnScriptEngine
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsAlreadyLockedError
|
||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsNotEnqueuedError
|
||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceQueueService
|
||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||
from spiffworkflow_backend.services.workflow_execution_service import TaskRunnability
|
||||
from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionServiceError
|
||||
from spiffworkflow_backend.services.workflow_service import WorkflowService
|
||||
from spiffworkflow_backend.specs.start_event import StartConfiguration
|
||||
|
||||
|
@ -563,5 +567,68 @@ class ProcessInstanceService:
|
|||
assigned_user_group_identifier=assigned_user_group_identifier,
|
||||
potential_owner_usernames=potential_owner_usernames,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
@classmethod
|
||||
def create_and_run_process_instance(
|
||||
cls,
|
||||
process_model: ProcessModelInfo,
|
||||
persistence_level: str,
|
||||
data_to_inject: dict | None = None,
|
||||
process_id_to_run: str | None = None,
|
||||
user: UserModel | None = None,
|
||||
) -> ProcessInstanceProcessor:
|
||||
process_instance = None
|
||||
if persistence_level == "none":
|
||||
user_id = user.id if user is not None else None
|
||||
process_instance = ProcessInstanceModel(
|
||||
status=ProcessInstanceStatus.not_started.value,
|
||||
process_initiator_id=user_id,
|
||||
process_model_identifier=process_model.id,
|
||||
process_model_display_name=process_model.display_name,
|
||||
persistence_level=persistence_level,
|
||||
)
|
||||
else:
|
||||
if user is None:
|
||||
raise Exception("User must be provided to create a persistent process instance")
|
||||
process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
|
||||
process_model.id, user
|
||||
)
|
||||
|
||||
processor = None
|
||||
try:
|
||||
# this is only creates new process instances so no need to worry about process instance migrations
|
||||
processor = ProcessInstanceProcessor(
|
||||
process_instance,
|
||||
script_engine=CustomBpmnScriptEngine(use_restricted_script_engine=False),
|
||||
process_id_to_run=process_id_to_run,
|
||||
)
|
||||
save_to_db = process_instance.persistence_level != "none"
|
||||
if data_to_inject is not None:
|
||||
processor.do_engine_steps(save=save_to_db, execution_strategy_name="run_current_ready_tasks")
|
||||
next_task = processor.next_task()
|
||||
DeepMerge.merge(next_task.data, data_to_inject)
|
||||
processor.do_engine_steps(save=save_to_db, execution_strategy_name="greedy")
|
||||
except (
|
||||
ApiError,
|
||||
ProcessInstanceIsNotEnqueuedError,
|
||||
ProcessInstanceIsAlreadyLockedError,
|
||||
WorkflowExecutionServiceError,
|
||||
) as e:
|
||||
ErrorHandlingService.handle_error(process_instance, e)
|
||||
raise e
|
||||
except Exception as e:
|
||||
ErrorHandlingService.handle_error(process_instance, e)
|
||||
# FIXME: this is going to point someone to the wrong task - it's misinformation for errors in sub-processes.
|
||||
# we need to recurse through all last tasks if the last task is a call activity or subprocess.
|
||||
if processor is not None:
|
||||
task = processor.bpmn_process_instance.last_task
|
||||
if task is not None:
|
||||
raise ApiError.from_task(
|
||||
error_code="unknown_exception",
|
||||
message=f"An unknown error occurred. Original error: {e}",
|
||||
status_code=400,
|
||||
task=task,
|
||||
) from e
|
||||
raise e
|
||||
return processor
|
||||
|
|
|
@ -15,6 +15,7 @@ from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
|
|||
from spiffworkflow_backend.models.principal import MissingPrincipalError
|
||||
from spiffworkflow_backend.models.principal import PrincipalModel
|
||||
from spiffworkflow_backend.models.user import SPIFF_GUEST_USER
|
||||
from spiffworkflow_backend.models.user import SPIFF_SYSTEM_USER
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
|
||||
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentNotFoundError
|
||||
|
@ -266,11 +267,21 @@ class UserService:
|
|||
|
||||
@classmethod
|
||||
def find_or_create_guest_user(cls, username: str = SPIFF_GUEST_USER, group_identifier: str = SPIFF_GUEST_GROUP) -> UserModel:
|
||||
guest_user: UserModel | None = UserModel.query.filter_by(
|
||||
user: UserModel | None = UserModel.query.filter_by(
|
||||
username=username, service="spiff_guest_service", service_id="spiff_guest_service_id"
|
||||
).first()
|
||||
if guest_user is None:
|
||||
guest_user = cls.create_user(username, "spiff_guest_service", "spiff_guest_service_id")
|
||||
cls.add_user_to_group_or_add_to_waiting(guest_user.username, group_identifier)
|
||||
if user is None:
|
||||
user = cls.create_user(username, "spiff_guest_service", "spiff_guest_service_id")
|
||||
cls.add_user_to_group_or_add_to_waiting(user.username, group_identifier)
|
||||
|
||||
return guest_user
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def find_or_create_system_user(cls, username: str = SPIFF_SYSTEM_USER) -> UserModel:
|
||||
user: UserModel | None = UserModel.query.filter_by(
|
||||
username=username, service="spiff_system_service", service_id="spiff_system_service_id"
|
||||
).first()
|
||||
if user is None:
|
||||
user = cls.create_user(username, "spiff_system_service", "spiff_system_service_id")
|
||||
|
||||
return user
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||
<bpmn:collaboration id="Collaboration_0vy9d1x">
|
||||
<bpmn:participant id="Participant_12dcof4" processRef="Process_message_start" />
|
||||
</bpmn:collaboration>
|
||||
<bpmn:process id="Process_message_start" isExecutable="true">
|
||||
<bpmn:startEvent id="message_start_event" name="Message start event" messageRef="[object Object]">
|
||||
<bpmn:outgoing>Flow_08cv33e</bpmn:outgoing>
|
||||
<bpmn:messageEventDefinition id="MessageEventDefinition_014kujv" messageRef="Message_0rcwl5q" />
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_08cv33e" sourceRef="message_start_event" targetRef="Event_1bsjcsi" />
|
||||
<bpmn:endEvent id="Event_1bsjcsi">
|
||||
<bpmn:incoming>Flow_08cv33e</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
</bpmn:process>
|
||||
<bpmn:message id="Message_0rcwl5q" name="message_one">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:messageVariable>the_message</spiffworkflow:messageVariable>
|
||||
</bpmn:extensionElements>
|
||||
</bpmn:message>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0vy9d1x">
|
||||
<bpmndi:BPMNShape id="Participant_12dcof4_di" bpmnElement="Participant_12dcof4" isHorizontal="true">
|
||||
<dc:Bounds x="30" y="70" width="600" height="250" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_1xm7wfz_di" bpmnElement="message_start_event">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="163" y="202" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_1bsjcsi_di" bpmnElement="Event_1bsjcsi">
|
||||
<dc:Bounds x="432" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_08cv33e_di" bpmnElement="Flow_08cv33e">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="432" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"description": "",
|
||||
"display_name": "simple_message_send_receive",
|
||||
"exception_notification_addresses": [],
|
||||
"fault_or_suspend_on_exception": "fault",
|
||||
"metadata_extraction_paths": null,
|
||||
"primary_file_name": "simple-message-send-receive.bpmn",
|
||||
"primary_process_id": "Process_simple_message_send_receive_ivod1hz"
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||
<bpmn:collaboration id="Collaboration_1sixwed" messages="[object Object],[object Object],[object Object]">
|
||||
<bpmn:participant id="Participant_1e3jwxw" processRef="Process_simple_message_send_receive_ivod1hz" />
|
||||
</bpmn:collaboration>
|
||||
<bpmn:process id="Process_simple_message_send_receive_ivod1hz" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_17db3yp</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:endEvent id="EndEvent_1">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:instructionsForEndUser>The process instance completed successfully.</spiffworkflow:instructionsForEndUser>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_12pkbxb</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sendTask id="send_message" name="Send message" messageRef="Message_1wmbbip">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:instructionsForEndUser>This is an example **Manual Task**. A **Manual Task** is designed to allow someone to complete a task outside of the system and then report back that it is complete. You can click the *Continue* button to proceed. When you are done running this process, you can edit the **Process Model** to include a:
|
||||
|
||||
* **Script Task** - write a short snippet of python code to update some data
|
||||
* **User Task** - generate a form that collects information from a user
|
||||
* **Service Task** - communicate with an external API to fetch or update some data.
|
||||
|
||||
You can also change the text you are reading here by updating the *Instructions* on this example manual task.</spiffworkflow:instructionsForEndUser>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_17db3yp</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_12pkbxb</bpmn:outgoing>
|
||||
</bpmn:sendTask>
|
||||
<bpmn:sequenceFlow id="Flow_17db3yp" sourceRef="StartEvent_1" targetRef="send_message" />
|
||||
<bpmn:sequenceFlow id="Flow_12pkbxb" sourceRef="send_message" targetRef="EndEvent_1" />
|
||||
</bpmn:process>
|
||||
<bpmn:message id="Message_1wmbbip" name="message_one">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:messagePayload>{
|
||||
"a": 1
|
||||
}</spiffworkflow:messagePayload>
|
||||
</bpmn:extensionElements>
|
||||
</bpmn:message>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_1sixwed">
|
||||
<bpmndi:BPMNShape id="Participant_1e3jwxw_di" bpmnElement="Participant_1e3jwxw" isHorizontal="true">
|
||||
<dc:Bounds x="50" y="52" width="600" height="250" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_14za570_di" bpmnElement="EndEvent_1">
|
||||
<dc:Bounds x="432" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1ktwyb2_di" bpmnElement="send_message">
|
||||
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_17db3yp_di" bpmnElement="Flow_17db3yp">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="270" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_12pkbxb_di" bpmnElement="Flow_12pkbxb">
|
||||
<di:waypoint x="370" y="177" />
|
||||
<di:waypoint x="432" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -1,4 +1,3 @@
|
|||
"""Test Process Api Blueprint."""
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
|
@ -37,8 +36,6 @@ from spiffworkflow_backend.services.user_service import UserService
|
|||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
# from spiffworkflow_backend.services.git_service import GitService
|
||||
|
||||
|
||||
class TestProcessApi(BaseTest):
|
||||
def test_returns_403_if_user_does_not_have_permission(
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import json
|
||||
from hashlib import sha256
|
||||
from hmac import HMAC
|
||||
|
||||
from connexion import FlaskApp # type: ignore
|
||||
from flask.app import Flask
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
|
||||
class TestWebhooksController(BaseTest):
|
||||
def test_webhook_runs_configured_process_model(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
load_test_spec(
|
||||
"test_group/simple_script",
|
||||
process_model_source_directory="simple_script",
|
||||
)
|
||||
request_data = json.dumps({"body": "THIS IS OUR REQEST"})
|
||||
encoded_signature = self._create_encoded_signature(app, request_data)
|
||||
|
||||
response = client.post(
|
||||
"/v1.0/webhook",
|
||||
headers={"X-Hub-Signature-256": f"sha256={encoded_signature}", "Content-type": "application/json"},
|
||||
data=request_data,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
def _create_encoded_signature(self, app: FlaskApp, request_data: str) -> str:
|
||||
secret = app.config["SPIFFWORKFLOW_BACKEND_GITHUB_WEBHOOK_SECRET"].encode()
|
||||
return HMAC(key=secret, msg=request_data.encode(), digestmod=sha256).hexdigest()
|
|
@ -1,8 +1,10 @@
|
|||
from flask import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
|
||||
from spiffworkflow_backend.models.message_triggerable_process_model import MessageTriggerableProcessModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.services.message_service import MessageService
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
|
@ -207,3 +209,46 @@ class TestMessageService(BaseTest):
|
|||
message_instances = MessageInstanceModel.query.all()
|
||||
assert len(message_instances) == 1
|
||||
assert message_instances[0].name == "travel_start_test_v2"
|
||||
|
||||
def test_can_send_a_message_with_non_persistent_process_instance(
|
||||
self,
|
||||
app: Flask,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
process_model_sender = load_test_spec(
|
||||
"test_group/simple-message-send",
|
||||
process_model_source_directory="simple-message-send-receive",
|
||||
bpmn_file_name="simple-message-send-receive.bpmn",
|
||||
)
|
||||
load_test_spec(
|
||||
"test_group/simple-message-receive",
|
||||
process_model_source_directory="simple-message-send-receive",
|
||||
bpmn_file_name="message_start_event.bpmn",
|
||||
)
|
||||
|
||||
message_triggerable_process_model = MessageTriggerableProcessModel.query.filter_by(message_name="message_one").first()
|
||||
assert message_triggerable_process_model is not None
|
||||
|
||||
processor = ProcessInstanceService.create_and_run_process_instance(
|
||||
process_model=process_model_sender,
|
||||
persistence_level="none",
|
||||
)
|
||||
assert processor.process_instance_model.process_model_identifier == "test_group/simple-message-send"
|
||||
|
||||
# ensure we commit the message instances
|
||||
db.session.commit()
|
||||
|
||||
message_instances = MessageInstanceModel.query.all()
|
||||
assert len(message_instances) == 1
|
||||
|
||||
MessageService.correlate_all_message_instances()
|
||||
|
||||
process_instances = ProcessInstanceModel.query.all()
|
||||
assert len(process_instances) == 1
|
||||
assert process_instances[0].status == ProcessInstanceStatus.complete.value
|
||||
assert process_instances[0].process_model_identifier == "test_group/simple-message-receive"
|
||||
|
||||
message_instances = MessageInstanceModel.query.all()
|
||||
assert len(message_instances) == 2
|
||||
mi_statuses = [mi.status for mi in message_instances]
|
||||
assert mi_statuses == ["completed", "completed"]
|
||||
|
|
Loading…
Reference in New Issue