diff --git a/.env.testing b/.env.testing new file mode 100644 index 0000000..d062926 --- /dev/null +++ b/.env.testing @@ -0,0 +1,12 @@ +APP_NAME="Synapse Sentinel Core" +APP_ENV=testing +APP_KEY=base64:uu9NaQgjKWHuLvyg3EZvLNYjVgrkunyitUMq88LDf4c= +APP_DEBUG=true +APP_URL=http://localhost + +DB_CONNECTION=sqlite +DB_DATABASE=:memory: + +CACHE_STORE=array +QUEUE_CONNECTION=sync +SESSION_DRIVER=array diff --git a/app/Events/CertificationCompleted.php b/app/Events/CertificationCompleted.php new file mode 100644 index 0000000..37f32dc --- /dev/null +++ b/app/Events/CertificationCompleted.php @@ -0,0 +1,20 @@ +validated('repository'), + sha: $request->validated('sha'), + verdict: $request->validated('verdict'), + reason: $request->validated('reason'), + checks: $request->validated('checks'), + triggeredBy: $request->validated('triggered_by'), + prNumber: $request->validated('pr_number'), + ); + + return response()->json(['status' => 'accepted']); + } +} diff --git a/app/Http/Requests/GateWebhookRequest.php b/app/Http/Requests/GateWebhookRequest.php new file mode 100644 index 0000000..b3e7115 --- /dev/null +++ b/app/Http/Requests/GateWebhookRequest.php @@ -0,0 +1,32 @@ + + */ + public function rules(): array + { + return [ + 'repository' => ['required', 'string'], + 'sha' => ['required', 'string'], + 'verdict' => ['required', Rule::in(['approved', 'rejected', 'escalate'])], + 'reason' => ['nullable', 'string'], + 'checks' => ['nullable', 'array'], + 'triggered_by' => ['nullable', 'string'], + 'pr_number' => ['nullable', 'integer'], + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php deleted file mode 100644 index 749c7b7..0000000 --- a/app/Models/User.php +++ /dev/null @@ -1,48 +0,0 @@ - */ - use HasFactory, Notifiable; - - /** - * The attributes that are mass assignable. - * - * @var list - */ - protected $fillable = [ - 'name', - 'email', - 'password', - ]; - - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; - - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } -} diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..c3928c5 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,6 +7,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php deleted file mode 100644 index 584104c..0000000 --- a/database/factories/UserFactory.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -class UserFactory extends Factory -{ - /** - * The current password being used by the factory. - */ - protected static ?string $password; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'name' => fake()->name(), - 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), - ]; - } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } -} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php deleted file mode 100644 index 05fb5d9..0000000 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ /dev/null @@ -1,49 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); - } -}; diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index b7355d7..d70f97a 100644 --- a/resources/views/welcome.blade.php +++ b/resources/views/welcome.blade.php @@ -1,277 +1,12 @@ - - - - - {{ config('app.name', 'Laravel') }} - - - - - - - @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) - @vite(['resources/css/app.css', 'resources/js/app.js']) - @else - - @endif - - -
- @if (Route::has('login')) - - @endif -
-
-
-
-

Let's get started

-

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

- - -
-
- {{-- Laravel Logo --}} - - - - - - - - - - - {{-- Light Mode 12 SVG --}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{-- Dark Mode 12 SVG --}} - -
-
-
-
- - @if (Route::has('login')) - - @endif - + + + + {{ config('app.name', 'Synapse Sentinel Core') }} + + +

Synapse Sentinel Core

+

Event intake system for the Synapse Sentinel ecosystem.

+ diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..bd1cfb1 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,9 @@ +name('webhooks.gate'); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 8fdc86b..c1cad07 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -1,7 +1,17 @@ get('/'); +declare(strict_types=1); - $response->assertStatus(200); +describe('Application', function () { + it('returns a successful response for the homepage', function () { + $response = $this->get('/'); + + $response->assertOk(); + }); + + it('has a health check endpoint', function () { + $response = $this->get('/up'); + + $response->assertOk(); + }); }); diff --git a/tests/Feature/WebhookGateTest.php b/tests/Feature/WebhookGateTest.php new file mode 100644 index 0000000..7c737b7 --- /dev/null +++ b/tests/Feature/WebhookGateTest.php @@ -0,0 +1,72 @@ + 'conduit-ui/pr', + 'sha' => 'abc123def456', + 'verdict' => 'approved', + 'reason' => 'All checks passed', + 'checks' => [ + 'tests' => ['status' => 'pass', 'coverage' => 100], + 'security' => ['status' => 'pass'], + 'syntax' => ['status' => 'pass'], + ], + 'triggered_by' => 'pull_request', + 'pr_number' => 42, + ]; + + $response = $this->postJson('/api/webhooks/gate', $payload); + + $response->assertSuccessful(); + $response->assertJson(['status' => 'accepted']); + }); + + it('rejects payload missing required repository field', function () { + $payload = [ + 'sha' => 'abc123', + 'verdict' => 'approved', + ]; + + $response = $this->postJson('/api/webhooks/gate', $payload); + + $response->assertUnprocessable(); + $response->assertJsonValidationErrors(['repository']); + }); + + it('rejects payload with invalid verdict value', function () { + $payload = [ + 'repository' => 'conduit-ui/pr', + 'sha' => 'abc123', + 'verdict' => 'invalid_verdict', + ]; + + $response = $this->postJson('/api/webhooks/gate', $payload); + + $response->assertUnprocessable(); + $response->assertJsonValidationErrors(['verdict']); + }); + + it('stores certification event via Laravel Verbs', function () { + $payload = [ + 'repository' => 'conduit-ui/pr', + 'sha' => 'abc123def456', + 'verdict' => 'approved', + 'reason' => 'All checks passed', + 'checks' => [ + 'tests' => ['status' => 'pass', 'coverage' => 100], + ], + 'triggered_by' => 'push', + ]; + + $response = $this->postJson('/api/webhooks/gate', $payload); + + $response->assertSuccessful(); + + $this->assertDatabaseHas('verb_events', [ + 'type' => 'App\Events\CertificationCompleted', + ]); + }); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 60f04a4..40d096b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -12,7 +12,7 @@ */ pest()->extend(Tests\TestCase::class) - // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) + ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->in('Feature'); /* diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php index 44a4f33..804f358 100644 --- a/tests/Unit/ExampleTest.php +++ b/tests/Unit/ExampleTest.php @@ -1,5 +1,9 @@ toBeTrue(); +declare(strict_types=1); + +describe('Unit', function () { + it('confirms basic assertions work', function () { + expect(true)->toBeTrue(); + }); });