diff --git a/README.md b/README.md index d1e0467..95023e0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Laravel Workflow is a package for the Laravel web framework that provides tools for defining and managing workflows and activities. A workflow is a series of interconnected activities that are executed in a specific order to achieve a desired result. Activities are individual tasks or pieces of logic that are executed as part of a workflow. -Laravel Workflow can be used to automate and manage complex processes, such as financial transactions, data analysis, data pipelines, microservices, job tracking, user signup flows, sagas and other business processes. By using Laravel Workflow, developers can break down large, complex processes into smaller, modular units that can be easily maintained and updated. +Laravel Workflow can be used to automate and manage complex processes, such as agentic workflows (AI-driven), financial transactions, data analysis, data pipelines, microservices, job tracking, user signup flows, sagas and other business processes. By using Laravel Workflow, developers can break down large, complex processes into smaller, modular units that can be easily maintained and updated. Some key features and benefits of Laravel Workflow include: diff --git a/src/Auth/NullAuthenticator.php b/src/Auth/NullAuthenticator.php new file mode 100644 index 0000000..ed0b086 --- /dev/null +++ b/src/Auth/NullAuthenticator.php @@ -0,0 +1,15 @@ +header(config('workflows.webhook_auth.signature.header')), + (string) hash_hmac('sha256', $request->getContent(), config('workflows.webhook_auth.signature.secret')) + )) { + abort(401, 'Unauthorized'); + } + return $request; + } +} diff --git a/src/Auth/TokenAuthenticator.php b/src/Auth/TokenAuthenticator.php new file mode 100644 index 0000000..caa50a0 --- /dev/null +++ b/src/Auth/TokenAuthenticator.php @@ -0,0 +1,21 @@ +header(config('workflows.webhook_auth.token.header')) + )) { + abort(401, 'Unauthorized'); + } + return $request; + } +} diff --git a/src/Auth/WebhookAuthenticator.php b/src/Auth/WebhookAuthenticator.php new file mode 100644 index 0000000..84051a7 --- /dev/null +++ b/src/Auth/WebhookAuthenticator.php @@ -0,0 +1,12 @@ +auth(); - - Http::withToken($auth['token']) - ->withHeaders([ - 'apiKey' => $auth['public'], - ]) - ->withOptions([ - 'query' => [ - 'id' => 'eq.' . $event->activityId, - ], - ]) - ->patch(config('workflows.monitor_url') . '/rest/v1/activities', [ - 'output' => $event->output, - 'status' => 'completed', - 'updated_at' => $event->timestamp, - ]); - } -} diff --git a/src/Listeners/MonitorActivityFailed.php b/src/Listeners/MonitorActivityFailed.php deleted file mode 100644 index e1d331c..0000000 --- a/src/Listeners/MonitorActivityFailed.php +++ /dev/null @@ -1,37 +0,0 @@ -auth(); - - Http::withToken($auth['token']) - ->withHeaders([ - 'apiKey' => $auth['public'], - ]) - ->withOptions([ - 'query' => [ - 'id' => 'eq.' . $event->activityId, - ], - ]) - ->patch(config('workflows.monitor_url') . '/rest/v1/activities', [ - 'output' => $event->output, - 'status' => 'failed', - 'updated_at' => $event->timestamp, - ]); - } -} diff --git a/src/Listeners/MonitorActivityStarted.php b/src/Listeners/MonitorActivityStarted.php deleted file mode 100644 index 60314c1..0000000 --- a/src/Listeners/MonitorActivityStarted.php +++ /dev/null @@ -1,37 +0,0 @@ -auth(); - - Http::withToken($auth['token']) - ->withHeaders([ - 'apiKey' => $auth['public'], - ]) - ->post(config('workflows.monitor_url') . '/rest/v1/activities', [ - 'id' => $event->activityId, - 'user_id' => $auth['user'], - 'workflow_id' => $event->workflowId, - 'class' => $event->class, - 'index' => $event->index, - 'arguments' => $event->arguments, - 'status' => 'running', - 'created_at' => $event->timestamp, - ]); - } -} diff --git a/src/Listeners/MonitorWorkflowCompleted.php b/src/Listeners/MonitorWorkflowCompleted.php deleted file mode 100644 index e9e77c8..0000000 --- a/src/Listeners/MonitorWorkflowCompleted.php +++ /dev/null @@ -1,38 +0,0 @@ -auth(); - - Http::withToken($auth['token']) - ->withHeaders([ - 'apiKey' => $auth['public'], - ]) - ->withOptions([ - 'query' => [ - 'user_id' => 'eq.' . $auth['user'], - 'workflow_id' => 'eq.' . $event->workflowId, - ], - ]) - ->patch(config('workflows.monitor_url') . '/rest/v1/workflows', [ - 'output' => $event->output, - 'status' => 'completed', - 'updated_at' => $event->timestamp, - ]); - } -} diff --git a/src/Listeners/MonitorWorkflowFailed.php b/src/Listeners/MonitorWorkflowFailed.php deleted file mode 100644 index 62d6422..0000000 --- a/src/Listeners/MonitorWorkflowFailed.php +++ /dev/null @@ -1,38 +0,0 @@ -auth(); - - Http::withToken($auth['token']) - ->withHeaders([ - 'apiKey' => $auth['public'], - ]) - ->withOptions([ - 'query' => [ - 'user_id' => 'eq.' . $auth['user'], - 'workflow_id' => 'eq.' . $event->workflowId, - ], - ]) - ->patch(config('workflows.monitor_url') . '/rest/v1/workflows', [ - 'output' => $event->output, - 'status' => 'failed', - 'updated_at' => $event->timestamp, - ]); - } -} diff --git a/src/Listeners/MonitorWorkflowStarted.php b/src/Listeners/MonitorWorkflowStarted.php deleted file mode 100644 index 7e6851d..0000000 --- a/src/Listeners/MonitorWorkflowStarted.php +++ /dev/null @@ -1,35 +0,0 @@ -auth(); - - Http::withToken($auth['token']) - ->withHeaders([ - 'apiKey' => $auth['public'], - ]) - ->post(config('workflows.monitor_url') . '/rest/v1/workflows', [ - 'user_id' => $auth['user'], - 'workflow_id' => $event->workflowId, - 'class' => $event->class, - 'arguments' => $event->arguments, - 'status' => 'running', - 'created_at' => $event->timestamp, - ]); - } -} diff --git a/src/Providers/WorkflowServiceProvider.php b/src/Providers/WorkflowServiceProvider.php index ba91d9d..945e1a1 100644 --- a/src/Providers/WorkflowServiceProvider.php +++ b/src/Providers/WorkflowServiceProvider.php @@ -5,23 +5,10 @@ namespace Workflow\Providers; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; use Laravel\SerializableClosure\SerializableClosure; use Workflow\Commands\ActivityMakeCommand; use Workflow\Commands\WorkflowMakeCommand; -use Workflow\Events\ActivityCompleted; -use Workflow\Events\ActivityFailed; -use Workflow\Events\ActivityStarted; -use Workflow\Events\WorkflowCompleted; -use Workflow\Events\WorkflowFailed; -use Workflow\Events\WorkflowStarted; -use Workflow\Listeners\MonitorActivityCompleted; -use Workflow\Listeners\MonitorActivityFailed; -use Workflow\Listeners\MonitorActivityStarted; -use Workflow\Listeners\MonitorWorkflowCompleted; -use Workflow\Listeners\MonitorWorkflowFailed; -use Workflow\Listeners\MonitorWorkflowStarted; final class WorkflowServiceProvider extends ServiceProvider { @@ -33,15 +20,6 @@ class_alias(config('workflows.base_model', Model::class), 'Workflow\Models\Model SerializableClosure::setSecretKey(config('app.key')); - if (config('workflows.monitor', false)) { - Event::listen(WorkflowStarted::class, [MonitorWorkflowStarted::class, 'handle']); - Event::listen(WorkflowCompleted::class, [MonitorWorkflowCompleted::class, 'handle']); - Event::listen(WorkflowFailed::class, [MonitorWorkflowFailed::class, 'handle']); - Event::listen(ActivityStarted::class, [MonitorActivityStarted::class, 'handle']); - Event::listen(ActivityCompleted::class, [MonitorActivityCompleted::class, 'handle']); - Event::listen(ActivityFailed::class, [MonitorActivityFailed::class, 'handle']); - } - $this->publishes([ __DIR__ . '/../config/workflows.php' => config_path('workflows.php'), ], 'config'); @@ -52,9 +30,4 @@ class_alias(config('workflows.base_model', Model::class), 'Workflow\Models\Model $this->commands([ActivityMakeCommand::class, WorkflowMakeCommand::class]); } - - public function register(): void - { - // - } } diff --git a/src/Traits/FetchesMonitorAuth.php b/src/Traits/FetchesMonitorAuth.php deleted file mode 100644 index e06546e..0000000 --- a/src/Traits/FetchesMonitorAuth.php +++ /dev/null @@ -1,20 +0,0 @@ -get(config('workflows.monitor_url') . '/functions/v1/get-user') - ->json(); - }); - } -} diff --git a/src/Traits/MonitorQueueConnection.php b/src/Traits/MonitorQueueConnection.php deleted file mode 100644 index 9bbcc76..0000000 --- a/src/Traits/MonitorQueueConnection.php +++ /dev/null @@ -1,21 +0,0 @@ -viaConnection() . '.queue', 'default') - ); - } -} diff --git a/src/Webhooks.php b/src/Webhooks.php index 559ac94..36220f2 100644 --- a/src/Webhooks.php +++ b/src/Webhooks.php @@ -9,6 +9,10 @@ use Illuminate\Support\Str; use ReflectionClass; use ReflectionMethod; +use Workflow\Auth\NullAuthenticator; +use Workflow\Auth\SignatureAuthenticator; +use Workflow\Auth\TokenAuthenticator; +use Workflow\Auth\WebhookAuthenticator; class Webhooks { @@ -75,12 +79,7 @@ private static function registerWorkflowWebhooks($workflow, $basePath) if ($method->getName() === 'execute') { $slug = Str::kebab(class_basename($workflow)); Route::post("{$basePath}/start/{$slug}", static function (Request $request) use ($workflow) { - if (! self::validateAuth($request)) { - return response()->json([ - 'error' => 'Unauthorized', - ], 401); - } - + $request = self::validateAuth($request); $params = self::resolveNamedParameters($workflow, 'execute', $request->all()); WorkflowStub::make($workflow)->start(...$params); return response()->json([ @@ -97,24 +96,17 @@ private static function registerSignalWebhooks($workflow, $basePath) if (self::hasWebhookAttributeOnMethod($method)) { $slug = Str::kebab(class_basename($workflow)); $signal = Str::kebab($method->getName()); - Route::post( "{$basePath}/signal/{$slug}/{workflowId}/{$signal}", static function (Request $request, $workflowId) use ($workflow, $method) { - if (! self::validateAuth($request)) { - return response()->json([ - 'error' => 'Unauthorized', - ], 401); - } - + $request = self::validateAuth($request); $workflowInstance = WorkflowStub::load($workflowId); $params = self::resolveNamedParameters( $workflow, $method->getName(), - $request->except('workflow_id') + $request->except('workflowId') ); $workflowInstance->{$method->getName()}(...$params); - return response()->json([ 'message' => 'Signal sent', ]); @@ -160,29 +152,20 @@ private static function resolveNamedParameters($class, $method, $payload) return $params; } - private static function validateAuth(Request $request): bool + private static function validateAuth(Request $request): Request { - $config = config('workflows.webhook_auth', [ - 'method' => 'none', - ]); - - if ($config['method'] === 'none') { - return true; - } - - if ($config['method'] === 'signature') { - $secret = $config['signature']['secret']; - $header = $config['signature']['header']; - $expectedSignature = hash_hmac('sha256', $request->getContent(), $secret); - return $request->header($header) === $expectedSignature; - } - - if ($config['method'] === 'token') { - $token = $config['token']['token']; - $header = $config['token']['header']; - return $request->header($header) === $token; + $authenticatorClass = match (config('workflows.webhook_auth.method', 'none')) { + 'none' => NullAuthenticator::class, + 'signature' => SignatureAuthenticator::class, + 'token' => TokenAuthenticator::class, + 'custom' => config('workflows.webhook_auth.custom.class'), + default => null, + }; + + if (! is_subclass_of($authenticatorClass, WebhookAuthenticator::class)) { + abort(401, 'Unauthorized'); } - return false; + return (new $authenticatorClass())->validate($request); } } diff --git a/src/config/workflows.php b/src/config/workflows.php index 702c9fc..c01308a 100644 --- a/src/config/workflows.php +++ b/src/config/workflows.php @@ -37,18 +37,9 @@ 'header' => env('WORKFLOW_WEBHOOKS_TOKEN_HEADER', 'Authorization'), 'token' => env('WORKFLOW_WEBHOOKS_TOKEN'), ], - ], - - 'monitor' => env('WORKFLOW_MONITOR', false), - - 'monitor_url' => env('WORKFLOW_MONITOR_URL'), - - 'monitor_api_key' => env('WORKFLOW_MONITOR_API_KEY'), - 'monitor_connection' => env('WORKFLOW_MONITOR_CONNECTION', config('queue.default')), - - 'monitor_queue' => env( - 'WORKFLOW_MONITOR_QUEUE', - config('queue.connections.' . config('queue.default') . '.queue', 'default') - ), + 'custom' => [ + 'class' => env('WORKFLOW_WEBHOOKS_CUSTOM_AUTH_CLASS', null), + ], + ], ]; diff --git a/tests/Feature/ExceptionWorkflowTest.php b/tests/Feature/ExceptionWorkflowTest.php index 30e4f02..29362f0 100644 --- a/tests/Feature/ExceptionWorkflowTest.php +++ b/tests/Feature/ExceptionWorkflowTest.php @@ -4,8 +4,8 @@ namespace Tests\Feature; -use Tests\Fixtures\NonRetryableTestExceptionWorkflow; use Tests\Fixtures\TestExceptionWorkflow; +use Tests\Fixtures\TestNonRetryableExceptionWorkflow; use Tests\TestCase; use Workflow\Serializers\Serializer; use Workflow\States\WorkflowCompletedStatus; @@ -34,7 +34,7 @@ public function testRetry(): void public function testNonRetryableException(): void { - $workflow = WorkflowStub::make(NonRetryableTestExceptionWorkflow::class); + $workflow = WorkflowStub::make(TestNonRetryableExceptionWorkflow::class); $workflow->start(); diff --git a/tests/Feature/WebhookWorkflowTest.php b/tests/Feature/WebhookWorkflowTest.php index 77cf570..252613e 100644 --- a/tests/Feature/WebhookWorkflowTest.php +++ b/tests/Feature/WebhookWorkflowTest.php @@ -111,7 +111,7 @@ public function testSignatureAuth() $response->assertStatus(401); $response->assertJson([ - 'error' => 'Unauthorized', + 'message' => 'Unauthorized', ]); $response = $this->postJson('/webhooks/start/test-webhook-workflow', [], [ @@ -120,7 +120,7 @@ public function testSignatureAuth() $response->assertStatus(401); $response->assertJson([ - 'error' => 'Unauthorized', + 'message' => 'Unauthorized', ]); $response = $this->postJson('/webhooks/start/test-webhook-workflow', [], [ @@ -166,7 +166,7 @@ public function testTokenAuth() $response->assertStatus(401); $response->assertJson([ - 'error' => 'Unauthorized', + 'message' => 'Unauthorized', ]); $response = $this->postJson('/webhooks/start/test-webhook-workflow', [], [ @@ -175,7 +175,7 @@ public function testTokenAuth() $response->assertStatus(401); $response->assertJson([ - 'error' => 'Unauthorized', + 'message' => 'Unauthorized', ]); $response = $this->postJson('/webhooks/start/test-webhook-workflow', [], [ diff --git a/tests/Fixtures/StateMachine.php b/tests/Fixtures/StateMachine.php index 6a42ea1..cd735a3 100644 --- a/tests/Fixtures/StateMachine.php +++ b/tests/Fixtures/StateMachine.php @@ -44,7 +44,7 @@ public function apply($action) if (isset($this->transitions[$action]) && $this->transitions[$action]['from'] === $this->currentState) { $this->currentState = $this->transitions[$action]['to']; } else { - throw new Exception('Transition not found,'); + throw new Exception('Transition not found.'); } } } diff --git a/tests/Fixtures/TestAuthenticator.php b/tests/Fixtures/TestAuthenticator.php new file mode 100644 index 0000000..604fe5f --- /dev/null +++ b/tests/Fixtures/TestAuthenticator.php @@ -0,0 +1,19 @@ +header('Authorization')) { + return $request; + } + abort(401, 'Unauthorized'); + } +} diff --git a/tests/Fixtures/NonRetryableTestExceptionActivity.php b/tests/Fixtures/TestNonRetryableExceptionActivity.php similarity index 79% rename from tests/Fixtures/NonRetryableTestExceptionActivity.php rename to tests/Fixtures/TestNonRetryableExceptionActivity.php index b6b1692..89a0d4e 100644 --- a/tests/Fixtures/NonRetryableTestExceptionActivity.php +++ b/tests/Fixtures/TestNonRetryableExceptionActivity.php @@ -7,7 +7,7 @@ use Workflow\Activity; use Workflow\Exceptions\NonRetryableException; -final class NonRetryableTestExceptionActivity extends Activity +final class TestNonRetryableExceptionActivity extends Activity { public function execute() { diff --git a/tests/Fixtures/NonRetryableTestExceptionWorkflow.php b/tests/Fixtures/TestNonRetryableExceptionWorkflow.php similarity index 67% rename from tests/Fixtures/NonRetryableTestExceptionWorkflow.php rename to tests/Fixtures/TestNonRetryableExceptionWorkflow.php index 2f13281..a72f910 100644 --- a/tests/Fixtures/NonRetryableTestExceptionWorkflow.php +++ b/tests/Fixtures/TestNonRetryableExceptionWorkflow.php @@ -7,11 +7,11 @@ use Workflow\ActivityStub; use Workflow\Workflow; -final class NonRetryableTestExceptionWorkflow extends Workflow +final class TestNonRetryableExceptionWorkflow extends Workflow { public function execute() { - yield ActivityStub::make(NonRetryableTestExceptionActivity::class); + yield ActivityStub::make(TestNonRetryableExceptionActivity::class); yield ActivityStub::make(TestActivity::class); return 'Workflow completes'; diff --git a/tests/Fixtures/TestThrowOnReturnWorkflow.php b/tests/Fixtures/TestThrowOnReturnWorkflow.php new file mode 100644 index 0000000..7ee9360 --- /dev/null +++ b/tests/Fixtures/TestThrowOnReturnWorkflow.php @@ -0,0 +1,20 @@ +assertSame(WorkflowFailedStatus::class, $workflow->status()); } + public function testNonRetryableExceptionActivity(): void + { + $this->expectException(NonRetryableException::class); + + $workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $activity = new TestNonRetryableExceptionActivity(0, now()->toDateTimeString(), StoredWorkflow::findOrFail( + $workflow->id() + )); + + $activity->handle(); + + $workflow->fresh(); + + $this->assertSame(1, $workflow->exceptions()->count()); + $this->assertSame(0, $workflow->logs()->count()); + $this->assertSame(WorkflowFailedStatus::class, $workflow->status()); + } + public function testFailedActivity(): void { $workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); diff --git a/tests/Unit/Config/WorkflowsConfigTest.php b/tests/Unit/Config/WorkflowsConfigTest.php index 96f9dbc..28ad829 100644 --- a/tests/Unit/Config/WorkflowsConfigTest.php +++ b/tests/Unit/Config/WorkflowsConfigTest.php @@ -26,14 +26,6 @@ public function testConfigIsLoaded(): void 'serializer' => \Workflow\Serializers\Y::class, 'prune_age' => '1 month', 'webhooks_route' => env('WORKFLOW_WEBHOOKS_ROUTE', 'webhooks'), - 'monitor' => env('WORKFLOW_MONITOR', false), - 'monitor_url' => env('WORKFLOW_MONITOR_URL'), - 'monitor_api_key' => env('WORKFLOW_MONITOR_API_KEY'), - 'monitor_connection' => env('WORKFLOW_MONITOR_CONNECTION', config('queue.default')), - 'monitor_queue' => env( - 'WORKFLOW_MONITOR_QUEUE', - config('queue.connections.' . config('queue.default') . '.queue', 'default') - ), ]; foreach ($expectedConfig as $key => $expectedValue) { diff --git a/tests/Unit/ExceptionTest.php b/tests/Unit/ExceptionTest.php new file mode 100644 index 0000000..dac9eef --- /dev/null +++ b/tests/Unit/ExceptionTest.php @@ -0,0 +1,46 @@ +toDateTimeString(), new StoredWorkflow(), new \Exception( + 'Test exception' + )); + + $middleware = collect($exception->middleware()) + ->map(static fn ($middleware) => is_object($middleware) ? get_class($middleware) : $middleware) + ->values(); + + $this->assertCount(1, $middleware); + $this->assertSame([WithoutOverlappingMiddleware::class], $middleware->all()); + } + + public function testExceptionWorkflowRunning(): void + { + $workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($workflow->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowRunningStatus::$name, + ]); + + $exception = new Exception(0, now()->toDateTimeString(), $storedWorkflow, new \Exception('Test exception')); + $exception->handle(); + + $this->assertSame(WorkflowRunningStatus::class, $workflow->status()); + } +} diff --git a/tests/Unit/Listeners/MonitorActivityCompletedTest.php b/tests/Unit/Listeners/MonitorActivityCompletedTest.php deleted file mode 100644 index 0e09da2..0000000 --- a/tests/Unit/Listeners/MonitorActivityCompletedTest.php +++ /dev/null @@ -1,59 +0,0 @@ -app->make('cache') - ->store() - ->clear(); - - config([ - 'workflows.monitor_url' => 'http://test', - ]); - config([ - 'workflows.monitor_api_key' => 'key', - ]); - - $activityId = (string) Str::uuid(); - - Http::fake([ - 'functions/v1/get-user' => Http::response([ - 'user' => 'user', - 'public' => 'public', - 'token' => 'token', - ]), - "rest/v1/activities?id=eq.{$activityId}" => Http::response(), - ]); - - $event = new ActivityCompleted(1, $activityId, 'output', 'time'); - $listener = new MonitorActivityCompleted(); - $listener->handle($event); - - Http::assertSent(static function (Request $request) { - return $request->hasHeader('Authorization', 'Bearer key') && - $request->url() === 'http://test/functions/v1/get-user'; - }); - - Http::assertSent(static function (Request $request) use ($activityId) { - $data = json_decode($request->body()); - return $request->hasHeader('apiKey', 'public') && - $request->hasHeader('Authorization', 'Bearer token') && - $request->url() === "http://test/rest/v1/activities?id=eq.{$activityId}" && - $data->status === 'completed' && - $data->output === 'output' && - $data->updated_at === 'time'; - }); - } -} diff --git a/tests/Unit/Listeners/MonitorActivityFailedTest.php b/tests/Unit/Listeners/MonitorActivityFailedTest.php deleted file mode 100644 index 994ad6d..0000000 --- a/tests/Unit/Listeners/MonitorActivityFailedTest.php +++ /dev/null @@ -1,59 +0,0 @@ -app->make('cache') - ->store() - ->clear(); - - config([ - 'workflows.monitor_url' => 'http://test', - ]); - config([ - 'workflows.monitor_api_key' => 'key', - ]); - - $activityId = (string) Str::uuid(); - - Http::fake([ - 'functions/v1/get-user' => Http::response([ - 'user' => 'user', - 'public' => 'public', - 'token' => 'token', - ]), - "rest/v1/activities?id=eq.{$activityId}" => Http::response(), - ]); - - $event = new ActivityFailed(1, $activityId, 'output', 'time'); - $listener = new MonitorActivityFailed(); - $listener->handle($event); - - Http::assertSent(static function (Request $request) { - return $request->hasHeader('Authorization', 'Bearer key') && - $request->url() === 'http://test/functions/v1/get-user'; - }); - - Http::assertSent(static function (Request $request) use ($activityId) { - $data = json_decode($request->body()); - return $request->hasHeader('apiKey', 'public') && - $request->hasHeader('Authorization', 'Bearer token') && - $request->url() === "http://test/rest/v1/activities?id=eq.{$activityId}" && - $data->status === 'failed' && - $data->output === 'output' && - $data->updated_at === 'time'; - }); - } -} diff --git a/tests/Unit/Listeners/MonitorActivityStartedTest.php b/tests/Unit/Listeners/MonitorActivityStartedTest.php deleted file mode 100644 index 6c44307..0000000 --- a/tests/Unit/Listeners/MonitorActivityStartedTest.php +++ /dev/null @@ -1,64 +0,0 @@ -app->make('cache') - ->store() - ->clear(); - - config([ - 'workflows.monitor_url' => 'http://test', - ]); - config([ - 'workflows.monitor_api_key' => 'key', - ]); - - $activityId = (string) Str::uuid(); - - Http::fake([ - 'functions/v1/get-user' => Http::response([ - 'user' => 'user', - 'public' => 'public', - 'token' => 'token', - ]), - 'rest/v1/activities' => Http::response(), - ]); - - $event = new ActivityStarted(1, $activityId, 'class', 0, 'arguments', 'time'); - $listener = new MonitorActivityStarted(); - $listener->handle($event); - - Http::assertSent(static function (Request $request) { - return $request->hasHeader('Authorization', 'Bearer key') && - $request->url() === 'http://test/functions/v1/get-user'; - }); - - Http::assertSent(static function (Request $request) use ($activityId) { - $data = json_decode($request->body()); - return $request->hasHeader('apiKey', 'public') && - $request->hasHeader('Authorization', 'Bearer token') && - $request->url() === 'http://test/rest/v1/activities' && - $data->user_id === 'user' && - $data->workflow_id === 1 && - $data->id === $activityId && - $data->index === 0 && - $data->class === 'class' && - $data->status === 'running' && - $data->arguments === 'arguments' && - $data->created_at === 'time'; - }); - } -} diff --git a/tests/Unit/Listeners/MonitorWorkflowCompletedTest.php b/tests/Unit/Listeners/MonitorWorkflowCompletedTest.php deleted file mode 100644 index 3e1159d..0000000 --- a/tests/Unit/Listeners/MonitorWorkflowCompletedTest.php +++ /dev/null @@ -1,56 +0,0 @@ -app->make('cache') - ->store() - ->clear(); - - config([ - 'workflows.monitor_url' => 'http://test', - ]); - config([ - 'workflows.monitor_api_key' => 'key', - ]); - - Http::fake([ - 'functions/v1/get-user' => Http::response([ - 'user' => 'user', - 'public' => 'public', - 'token' => 'token', - ]), - 'rest/v1/workflows?user_id=eq.user&workflow_id=eq.1' => Http::response(), - ]); - - $event = new WorkflowCompleted(1, 'output', 'time'); - $listener = new MonitorWorkflowCompleted(); - $listener->handle($event); - - Http::assertSent(static function (Request $request) { - return $request->hasHeader('Authorization', 'Bearer key') && - $request->url() === 'http://test/functions/v1/get-user'; - }); - - Http::assertSent(static function (Request $request) { - $data = json_decode($request->body()); - return $request->hasHeader('apiKey', 'public') && - $request->hasHeader('Authorization', 'Bearer token') && - $request->url() === 'http://test/rest/v1/workflows?user_id=eq.user&workflow_id=eq.1' && - $data->status === 'completed' && - $data->output === 'output' && - $data->updated_at === 'time'; - }); - } -} diff --git a/tests/Unit/Listeners/MonitorWorkflowFailedTest.php b/tests/Unit/Listeners/MonitorWorkflowFailedTest.php deleted file mode 100644 index ffd5e5a..0000000 --- a/tests/Unit/Listeners/MonitorWorkflowFailedTest.php +++ /dev/null @@ -1,56 +0,0 @@ -app->make('cache') - ->store() - ->clear(); - - config([ - 'workflows.monitor_url' => 'http://test', - ]); - config([ - 'workflows.monitor_api_key' => 'key', - ]); - - Http::fake([ - 'functions/v1/get-user' => Http::response([ - 'user' => 'user', - 'public' => 'public', - 'token' => 'token', - ]), - 'rest/v1/workflows?user_id=eq.user&workflow_id=eq.1' => Http::response(), - ]); - - $event = new WorkflowFailed(1, 'output', 'time'); - $listener = new MonitorWorkflowFailed(); - $listener->handle($event); - - Http::assertSent(static function (Request $request) { - return $request->hasHeader('Authorization', 'Bearer key') && - $request->url() === 'http://test/functions/v1/get-user'; - }); - - Http::assertSent(static function (Request $request) { - $data = json_decode($request->body()); - return $request->hasHeader('apiKey', 'public') && - $request->hasHeader('Authorization', 'Bearer token') && - $request->url() === 'http://test/rest/v1/workflows?user_id=eq.user&workflow_id=eq.1' && - $data->status === 'failed' && - $data->output === 'output' && - $data->updated_at === 'time'; - }); - } -} diff --git a/tests/Unit/Listeners/MonitorWorkflowStartedTest.php b/tests/Unit/Listeners/MonitorWorkflowStartedTest.php deleted file mode 100644 index 6c2a24e..0000000 --- a/tests/Unit/Listeners/MonitorWorkflowStartedTest.php +++ /dev/null @@ -1,59 +0,0 @@ -app->make('cache') - ->store() - ->clear(); - - config([ - 'workflows.monitor_url' => 'http://test', - ]); - config([ - 'workflows.monitor_api_key' => 'key', - ]); - - Http::fake([ - 'functions/v1/get-user' => Http::response([ - 'user' => 'user', - 'public' => 'public', - 'token' => 'token', - ]), - 'rest/v1/workflows' => Http::response(), - ]); - - $event = new WorkflowStarted(1, 'class', 'arguments', 'time'); - $listener = new MonitorWorkflowStarted(); - $listener->handle($event); - - Http::assertSent(static function (Request $request) { - return $request->hasHeader('Authorization', 'Bearer key') && - $request->url() === 'http://test/functions/v1/get-user'; - }); - - Http::assertSent(static function (Request $request) { - $data = json_decode($request->body()); - return $request->hasHeader('apiKey', 'public') && - $request->hasHeader('Authorization', 'Bearer token') && - $request->url() === 'http://test/rest/v1/workflows' && - $data->user_id === 'user' && - $data->workflow_id === 1 && - $data->class === 'class' && - $data->status === 'running' && - $data->arguments === 'arguments' && - $data->created_at === 'time'; - }); - } -} diff --git a/tests/Unit/Middleware/WithoutOverlappingMiddlewareTest.php b/tests/Unit/Middleware/WithoutOverlappingMiddlewareTest.php index c0502a2..7fe6571 100644 --- a/tests/Unit/Middleware/WithoutOverlappingMiddlewareTest.php +++ b/tests/Unit/Middleware/WithoutOverlappingMiddlewareTest.php @@ -4,6 +4,8 @@ namespace Tests\Unit\Middleware; +use Illuminate\Contracts\Cache\Lock; +use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; use Mockery\MockInterface; use Tests\Fixtures\TestActivity; @@ -111,4 +113,60 @@ public function testAllowsMultipleActivityInstances(): void $this->assertNull(Cache::get($middleware1->getWorkflowSemaphoreKey())); $this->assertSame(0, count(Cache::get($middleware1->getActivitySemaphoreKey()))); } + + public function testUnknownTypeDoesNotCallNext(): void + { + $this->app->make('cache') + ->store() + ->clear(); + + $job = $this->mock(TestActivity::class, static function (MockInterface $mock) { + $mock->shouldReceive('release') + ->once(); + }); + + $middleware = new WithoutOverlappingMiddleware(1, 999); + + $middleware->handle($job, function ($job) { + $this->fail('Should not call next when type is unknown'); + }); + + $this->assertNull(Cache::get($middleware->getWorkflowSemaphoreKey())); + $this->assertNull(Cache::get($middleware->getActivitySemaphoreKey())); + } + + public function testReleaseWhenCompareAndSetFails(): void + { + $this->app->make('cache') + ->store() + ->clear(); + + $job = $this->mock(TestWorkflow::class, static function (MockInterface $mock) { + $mock->shouldReceive('release') + ->once(); + }); + + $lock = $this->mock(Lock::class, static function (MockInterface $mock) { + $mock->shouldReceive('get') + ->once() + ->andReturn(false); + }); + + $cache = $this->mock(Repository::class, static function (MockInterface $mock) use ($lock) { + $mock->shouldReceive('lock') + ->once() + ->andReturn($lock); + $mock->shouldReceive('get') + ->andReturn([]); + }); + + $middleware = new WithoutOverlappingMiddleware(1, WithoutOverlappingMiddleware::WORKFLOW); + + $middleware->handle($job, function ($job) { + $this->fail('Should not call next when lock is not acquired'); + }); + + $this->assertNull(Cache::get($middleware->getWorkflowSemaphoreKey())); + $this->assertNull(Cache::get($middleware->getActivitySemaphoreKey())); + } } diff --git a/tests/Unit/Providers/WorkflowServiceProviderTest.php b/tests/Unit/Providers/WorkflowServiceProviderTest.php index 8cbf185..7148c48 100644 --- a/tests/Unit/Providers/WorkflowServiceProviderTest.php +++ b/tests/Unit/Providers/WorkflowServiceProviderTest.php @@ -6,12 +6,6 @@ use Illuminate\Support\Facades\Artisan; use Tests\TestCase; -use Workflow\Events\ActivityCompleted; -use Workflow\Events\ActivityFailed; -use Workflow\Events\ActivityStarted; -use Workflow\Events\WorkflowCompleted; -use Workflow\Events\WorkflowFailed; -use Workflow\Events\WorkflowStarted; use Workflow\Providers\WorkflowServiceProvider; final class WorkflowServiceProviderTest extends TestCase @@ -29,47 +23,6 @@ public function testProviderLoads(): void ); } - public function testEventListenersAreRegistered(): void - { - config([ - 'workflows.monitor' => true, - ]); - - (new WorkflowServiceProvider($this->app))->boot(); - - $dispatcher = app('events'); - - $expectedListeners = [ - WorkflowStarted::class => \Workflow\Listeners\MonitorWorkflowStarted::class, - WorkflowCompleted::class => \Workflow\Listeners\MonitorWorkflowCompleted::class, - WorkflowFailed::class => \Workflow\Listeners\MonitorWorkflowFailed::class, - ActivityStarted::class => \Workflow\Listeners\MonitorActivityStarted::class, - ActivityCompleted::class => \Workflow\Listeners\MonitorActivityCompleted::class, - ActivityFailed::class => \Workflow\Listeners\MonitorActivityFailed::class, - ]; - - foreach ($expectedListeners as $event => $listener) { - $registeredListeners = $dispatcher->getListeners($event); - - $attached = false; - foreach ($registeredListeners as $registeredListener) { - if ($registeredListener instanceof \Closure) { - $closureReflection = new \ReflectionFunction($registeredListener); - $useVariables = $closureReflection->getStaticVariables(); - - if (isset($useVariables['listener']) && is_array($useVariables['listener'])) { - if ($useVariables['listener'][0] === $listener) { - $attached = true; - break; - } - } - } - } - - $this->assertTrue($attached, "Event [{$event}] does not have the [{$listener}] listener attached."); - } - } - public function testConfigIsPublished(): void { Artisan::call('vendor:publish', [ diff --git a/tests/Unit/Serializers/SerializeTest.php b/tests/Unit/Serializers/SerializeTest.php index 3f35e54..399e038 100644 --- a/tests/Unit/Serializers/SerializeTest.php +++ b/tests/Unit/Serializers/SerializeTest.php @@ -58,6 +58,13 @@ public static function dataProvider(): array ]; } + public function testSerializableReturnsFalseForClosure(): void + { + $this->assertFalse(Serializer::serializable(static function () { + return 'test'; + })); + } + private function testSerializeUnserialize($data, $serializer, $unserializer): void { config([ diff --git a/tests/Unit/SignalTest.php b/tests/Unit/SignalTest.php new file mode 100644 index 0000000..6470acc --- /dev/null +++ b/tests/Unit/SignalTest.php @@ -0,0 +1,44 @@ +middleware()) + ->map(static fn ($middleware) => is_object($middleware) ? get_class($middleware) : $middleware) + ->values(); + + $this->assertCount(1, $middleware); + $this->assertSame([WithoutOverlappingMiddleware::class], $middleware->all()); + } + + public function testSignalWorkflowRunning(): void + { + $workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($workflow->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowRunningStatus::$name, + ]); + + $signal = new Signal($storedWorkflow); + $signal->handle(); + + $this->assertSame(WorkflowRunningStatus::class, $workflow->status()); + } +} diff --git a/tests/Unit/Traits/TimersTest.php b/tests/Unit/Traits/TimersTest.php index cb92d77..3b0363a 100644 --- a/tests/Unit/Traits/TimersTest.php +++ b/tests/Unit/Traits/TimersTest.php @@ -77,7 +77,7 @@ public function testDefersIfNotElapsed(): void $this->assertNull($result); $this->assertSame(0, $workflow->logs()->count()); - WorkflowStub::timer('1 minute', static fn () => false) + WorkflowStub::awaitWithTimeout('1 minute', static fn () => false) ->then(static function ($value) use (&$result) { $result = $value; }); @@ -128,7 +128,7 @@ public function testLoadsStoredResult(): void 'result' => Serializer::serialize(true), ]); - WorkflowStub::timer('1 minute', static fn () => true) + WorkflowStub::awaitWithTimeout('1 minute', static fn () => true) ->then(static function ($value) use (&$result) { $result = $value; }); diff --git a/tests/Unit/WebhooksTest.php b/tests/Unit/WebhooksTest.php index 1e762a1..c811e6e 100644 --- a/tests/Unit/WebhooksTest.php +++ b/tests/Unit/WebhooksTest.php @@ -9,6 +9,8 @@ use Mockery; use ReflectionClass; use ReflectionMethod; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Tests\Fixtures\TestAuthenticator; use Tests\TestCase; use Workflow\Models\StoredWorkflow; use Workflow\States\WorkflowPendingStatus; @@ -126,7 +128,7 @@ public function testValidatesAuthForNone() $method = $webhooksReflection->getMethod('validateAuth'); $method->setAccessible(true); - $this->assertTrue($method->invoke(null, $request)); + $this->assertInstanceOf(Request::class, $method->invoke(null, $request)); } public function testValidatesAuthForTokenSuccess() @@ -145,11 +147,13 @@ public function testValidatesAuthForTokenSuccess() $method = $webhooksReflection->getMethod('validateAuth'); $method->setAccessible(true); - $this->assertTrue($method->invoke(null, $request)); + $this->assertInstanceOf(Request::class, $method->invoke(null, $request)); } public function testValidatesAuthForTokenFailure() { + $this->expectException(HttpException::class); + config([ 'workflows.webhook_auth.method' => 'token', 'workflows.webhook_auth.token.token' => 'valid-token', @@ -164,7 +168,7 @@ public function testValidatesAuthForTokenFailure() $method = $webhooksReflection->getMethod('validateAuth'); $method->setAccessible(true); - $this->assertFalse($method->invoke(null, $request)); + $method->invoke(null, $request); } public function testValidatesAuthForSignatureSuccess() @@ -188,11 +192,13 @@ public function testValidatesAuthForSignatureSuccess() $method = $webhooksReflection->getMethod('validateAuth'); $method->setAccessible(true); - $this->assertTrue($method->invoke(null, $request)); + $this->assertInstanceOf(Request::class, $method->invoke(null, $request)); } public function testValidatesAuthForSignatureFailure() { + $this->expectException(HttpException::class); + config([ 'workflows.webhook_auth.method' => 'signature', 'workflows.webhook_auth.signature.secret' => 'test-secret', @@ -212,7 +218,43 @@ public function testValidatesAuthForSignatureFailure() $method = $webhooksReflection->getMethod('validateAuth'); $method->setAccessible(true); - $this->assertFalse($method->invoke(null, $request)); + $method->invoke(null, $request); + } + + public function testValidatesAuthForCustomSuccess() + { + config([ + 'workflows.webhook_auth.method' => 'custom', + 'workflows.webhook_auth.custom.class' => TestAuthenticator::class, + ]); + + $request = Request::create('/webhooks/start/test-webhook-workflow', 'POST', [], [], [], [ + 'HTTP_AUTHORIZATION' => 'valid-token', + ]); + + $webhooksReflection = new ReflectionClass(Webhooks::class); + $method = $webhooksReflection->getMethod('validateAuth'); + $method->setAccessible(true); + + $this->assertInstanceOf(Request::class, $method->invoke(null, $request)); + } + + public function testValidatesAuthForCustomFailure() + { + $this->expectException(HttpException::class); + + config([ + 'workflows.webhook_auth.method' => 'custom', + 'workflows.webhook_auth.custom.class' => TestAuthenticator::class, + ]); + + $request = Request::create('/webhooks/start/test-webhook-workflow', 'POST'); + + $webhooksReflection = new ReflectionClass(Webhooks::class); + $method = $webhooksReflection->getMethod('validateAuth'); + $method->setAccessible(true); + + $method->invoke(null, $request); } public function testResolveNamedParameters() @@ -236,6 +278,8 @@ public function testResolveNamedParameters() public function testUnauthorizedRequestFails() { + $this->expectException(HttpException::class); + config([ 'workflows.webhook_auth.method' => 'unsupported', ]); @@ -246,7 +290,7 @@ public function testUnauthorizedRequestFails() $method = $webhooksReflection->getMethod('validateAuth'); $method->setAccessible(true); - $this->assertFalse($method->invoke(null, $request)); + $method->invoke(null, $request); } public function testResolveNamedParametersUsesDefaults() @@ -323,7 +367,7 @@ public function testStartUnauthorized(): void $response->assertStatus(401); $response->assertJson([ - 'error' => 'Unauthorized', + 'message' => 'Unauthorized', ]); } @@ -350,7 +394,7 @@ public function testSignalUnauthorized(): void $response->assertStatus(401); $response->assertJson([ - 'error' => 'Unauthorized', + 'message' => 'Unauthorized', ]); } } diff --git a/tests/Unit/WorkflowTest.php b/tests/Unit/WorkflowTest.php index 08eeace..3f95402 100644 --- a/tests/Unit/WorkflowTest.php +++ b/tests/Unit/WorkflowTest.php @@ -4,22 +4,133 @@ namespace Tests\Unit; +use BadMethodCallException; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Event; use Tests\Fixtures\TestActivity; use Tests\Fixtures\TestChildWorkflow; use Tests\Fixtures\TestOtherActivity; use Tests\Fixtures\TestParentWorkflow; +use Tests\Fixtures\TestThrowOnReturnWorkflow; use Tests\Fixtures\TestWorkflow; +use Tests\Fixtures\TestYieldNonPromiseWorkflow; use Tests\TestCase; +use Workflow\Events\WorkflowFailed; use Workflow\Exception; use Workflow\Models\StoredWorkflow; use Workflow\Serializers\Serializer; use Workflow\States\WorkflowCompletedStatus; +use Workflow\States\WorkflowFailedStatus; use Workflow\States\WorkflowPendingStatus; +use Workflow\Workflow; use Workflow\WorkflowStub; final class WorkflowTest extends TestCase { + public function testFailed(): void + { + Event::fake(); + + $parentWorkflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $storedParentWorkflow = StoredWorkflow::findOrFail($parentWorkflow->id()); + $storedParentWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowPendingStatus::$name, + ]); + + $stub = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($stub->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowPendingStatus::class, + ]); + + $storedWorkflow->parents() + ->attach($storedParentWorkflow, [ + 'parent_index' => 0, + 'parent_now' => now(), + ]); + + $workflow = new Workflow($storedWorkflow); + + $workflow->failed(new \Exception('Test exception')); + + $this->assertSame(WorkflowFailedStatus::class, $stub->status()); + $this->assertSame( + 'Test exception', + Serializer::unserialize($stub->exceptions()->first()->exception)['message'] + ); + + Event::assertDispatched(WorkflowFailed::class, static function ($event) use ($stub) { + return $event->workflowId === $stub->id(); + }); + } + + public function testFailedTwice(): void + { + Event::fake(); + + $stub = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($stub->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowFailedStatus::class, + ]); + + $workflow = new Workflow($storedWorkflow); + + $workflow->failed(new \Exception('Test exception')); + + $this->assertSame(WorkflowFailedStatus::class, $stub->status()); + $this->assertSame( + 'Test exception', + Serializer::unserialize($stub->exceptions()->first()->exception)['message'] + ); + + Event::assertNotDispatched(WorkflowFailed::class, static function ($event) use ($stub) { + return $event->workflowId === $stub->id(); + }); + } + + public function testFailedWithParentFailed(): void + { + Event::fake(); + + $parentWorkflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $storedParentWorkflow = StoredWorkflow::findOrFail($parentWorkflow->id()); + $storedParentWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowFailedStatus::$name, + ]); + + $stub = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($stub->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowPendingStatus::class, + ]); + + $storedWorkflow->parents() + ->attach($storedParentWorkflow, [ + 'parent_index' => 0, + 'parent_now' => now(), + ]); + + $workflow = new Workflow($storedWorkflow); + + $workflow->failed(new \Exception('Test exception')); + + $this->assertSame(WorkflowFailedStatus::class, $stub->status()); + $this->assertSame( + 'Test exception', + Serializer::unserialize($stub->exceptions()->first()->exception)['message'] + ); + + Event::assertDispatched(WorkflowFailed::class, static function ($event) use ($stub) { + return $event->workflowId === $stub->id(); + }); + } + public function testException(): void { $exception = new \Exception('test'); @@ -168,4 +279,52 @@ public function testParentPending(): void $this->assertSame(WorkflowCompletedStatus::class, $childWorkflow->status()); $this->assertSame('other', $childWorkflow->output()); } + + public function testThrowsWhenExecuteMethodIsMissing(): void + { + $stub = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($stub->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowPendingStatus::class, + ]); + + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Execute method not implemented.'); + + $workflow = new Workflow($storedWorkflow); + $workflow->handle(); + } + + public function testThrowsWhenYieldNonPromise(): void + { + $stub = WorkflowStub::load(WorkflowStub::make(TestYieldNonPromiseWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($stub->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowPendingStatus::class, + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('something went wrong'); + + $workflow = new TestYieldNonPromiseWorkflow($storedWorkflow); + $workflow->handle(); + } + + public function testThrowsWrappedException(): void + { + $stub = WorkflowStub::load(WorkflowStub::make(TestThrowOnReturnWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($stub->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowPendingStatus::class, + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Workflow failed.'); + + $workflow = new TestThrowOnReturnWorkflow($storedWorkflow); + $workflow->handle(); + } } diff --git a/tests/Unit/migrations/MigrationsTest.php b/tests/Unit/migrations/MigrationsTest.php new file mode 100644 index 0000000..ae24cc0 --- /dev/null +++ b/tests/Unit/migrations/MigrationsTest.php @@ -0,0 +1,33 @@ +assertTrue(Schema::hasTable('workflows')); + $this->assertTrue(Schema::hasTable('workflow_logs')); + $this->assertTrue(Schema::hasTable('workflow_signals')); + $this->assertTrue(Schema::hasTable('workflow_timers')); + $this->assertTrue(Schema::hasTable('workflow_exceptions')); + $this->assertTrue(Schema::hasTable('workflow_relationships')); + + $this->artisan('migrate:reset', [ + '--path' => dirname(__DIR__, 3) . '/src/migrations', + '--realpath' => true, + ])->run(); + + $this->assertFalse(Schema::hasTable('workflows')); + $this->assertFalse(Schema::hasTable('workflow_logs')); + $this->assertFalse(Schema::hasTable('workflow_signals')); + $this->assertFalse(Schema::hasTable('workflow_timers')); + $this->assertFalse(Schema::hasTable('workflow_exceptions')); + $this->assertFalse(Schema::hasTable('workflow_relationships')); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 00d2f11..25b78bc 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -3,7 +3,6 @@ declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; -require_once __DIR__ . '/classAliases.php'; use PHPUnit\Event\Facade; use Tests\TestSuiteSubscriber; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy