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:
jasquat 2024-02-12 13:44:52 -05:00 committed by GitHub
parent 73f7ab8ff4
commit cc3d10f340
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 407 additions and 90 deletions

View File

@ -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"

View File

@ -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

View File

@ -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")

View File

@ -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")

View File

@ -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"

View File

@ -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:

View File

@ -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

View File

@ -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",

View 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)

View File

@ -241,6 +241,7 @@ class AuthorizationService:
"task_allows_guest",
"test_raise_error",
"url_info",
"webhook",
]
if request.method == "OPTIONS":
return True

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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"
}

View File

@ -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>

View File

@ -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(

View File

@ -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()

View File

@ -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"]