diff --git a/connector-proxy-demo/poetry.lock b/connector-proxy-demo/poetry.lock
index 98c0298f4..0c1a68a7b 100644
--- a/connector-proxy-demo/poetry.lock
+++ b/connector-proxy-demo/poetry.lock
@@ -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"
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
index 5cfd234b1..7695b0a16 100755
--- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
@@ -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
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py
index 4716aacfd..394c01649 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py
@@ -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")
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/unit_testing.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/unit_testing.py
index 166632e80..26824dbac 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/config/unit_testing.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/unit_testing.py
@@ -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")
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py
index a3b3217da..9f2f7884a 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py
@@ -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"
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py
index 2231f6d7e..282664576 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py
@@ -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:
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py
index e40befedf..e7bef5679 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py
@@ -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
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py
index 5caad59df..6b8eb212d 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py
@@ -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",
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/webhooks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/webhooks_controller.py
new file mode 100644
index 000000000..2ccb95c1b
--- /dev/null
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/webhooks_controller.py
@@ -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)
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py
index 78daa85a9..810bf98d0 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py
@@ -241,6 +241,7 @@ class AuthorizationService:
"task_allows_guest",
"test_raise_error",
"url_info",
+ "webhook",
]
if request.method == "OPTIONS":
return True
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/message_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/message_service.py
index 58c90ba92..60e03607d 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/message_service.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/message_service.py
@@ -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",
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py
index bba7b50bb..240da1cf8 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py
@@ -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
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py
index 476f1a7cd..76ef5d0f4 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py
@@ -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
diff --git a/spiffworkflow-backend/tests/data/simple-message-send-receive/message_start_event.bpmn b/spiffworkflow-backend/tests/data/simple-message-send-receive/message_start_event.bpmn
new file mode 100644
index 000000000..b3e024106
--- /dev/null
+++ b/spiffworkflow-backend/tests/data/simple-message-send-receive/message_start_event.bpmn
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+ Flow_08cv33e
+
+
+
+
+ Flow_08cv33e
+
+
+
+
+ the_message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spiffworkflow-backend/tests/data/simple-message-send-receive/process_model.json b/spiffworkflow-backend/tests/data/simple-message-send-receive/process_model.json
new file mode 100644
index 000000000..5925b1ef4
--- /dev/null
+++ b/spiffworkflow-backend/tests/data/simple-message-send-receive/process_model.json
@@ -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"
+}
diff --git a/spiffworkflow-backend/tests/data/simple-message-send-receive/simple-message-send-receive.bpmn b/spiffworkflow-backend/tests/data/simple-message-send-receive/simple-message-send-receive.bpmn
new file mode 100644
index 000000000..9c2a80b15
--- /dev/null
+++ b/spiffworkflow-backend/tests/data/simple-message-send-receive/simple-message-send-receive.bpmn
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+ Flow_17db3yp
+
+
+
+ The process instance completed successfully.
+
+ Flow_12pkbxb
+
+
+
+ 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.
+
+ Flow_17db3yp
+ Flow_12pkbxb
+
+
+
+
+
+
+ {
+ "a": 1
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py
index da7e8c9f8..65a461283 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py
@@ -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(
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_webhooks_controller.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_webhooks_controller.py
new file mode 100644
index 000000000..d29abb0eb
--- /dev/null
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_webhooks_controller.py
@@ -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()
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py
index e4ae82623..78853cec5 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py
@@ -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"]