diff --git a/app/Events/PluginErrorReceivedEvent.php b/app/Events/PluginErrorReceivedEvent.php new file mode 100644 index 000000000..31f72ba3c --- /dev/null +++ b/app/Events/PluginErrorReceivedEvent.php @@ -0,0 +1,26 @@ +error = $error; + } + + /** + * @return PluginError + */ + public function getError(): PluginError + { + return $this->error; + } +} diff --git a/app/Exceptions/PluginErrorException.php b/app/Exceptions/PluginErrorException.php new file mode 100644 index 000000000..b042b431d --- /dev/null +++ b/app/Exceptions/PluginErrorException.php @@ -0,0 +1,14 @@ +checkForSuppliedData( + $request, + [ + 'user_report' => 'required|boolean', + 'data' => 'required|array', + ] + ); + + if ($dataCheck) { + return response(null, 400); + } + + $pluginError = PluginError::create( + [ + 'user_id' => Auth::user()->id, + 'user_report' => $request->json('user_report'), + 'data' => json_encode($request->json('data')), + ] + ); + + Event::fire(new PluginErrorReceivedEvent($pluginError)); + return response('', 204); + } +} diff --git a/app/Listeners/PluginError/RecordPluginErrorInBugsnag.php b/app/Listeners/PluginError/RecordPluginErrorInBugsnag.php new file mode 100644 index 000000000..433266fdb --- /dev/null +++ b/app/Listeners/PluginError/RecordPluginErrorInBugsnag.php @@ -0,0 +1,23 @@ +increments('id'); + $table->unsignedInteger('user_id')->comment('The user who the report is attributed to'); + $table->boolean('user_report')->comment('Whether the report was generated by the user or the plugin'); + $table->json('data')->comment('Report data'); + $table->timestamps(); + + $table->foreign('user_id') + ->references('id')->on('user'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('plugin_error'); + } +} diff --git a/routes/routes.php b/routes/routes.php index 5448f1ccb..e7038145d 100644 --- a/routes/routes.php +++ b/routes/routes.php @@ -15,7 +15,7 @@ 'uses' => 'TeapotController@teapot', ] ); - + // Version checking $router->get( 'version/{version:[A-Za-z0-9\.\-]+}/status', @@ -26,17 +26,20 @@ 'uses' => 'VersionController@getVersionStatus' ] ); - + // Dependencies $router->get('dependency', 'DependencyController@getManifest'); - + // Squawks $router->get('squawk-assignment/{callsign:[A-Za-z0-9\-]{1,10}}', 'SquawkController@getSquawkAssignment'); $router->put('squawk-assignment/{callsign:[A-Za-z0-9\-]{1,10}}', 'SquawkController@assignSquawk'); $router->delete('squawk-assignment/{callsign:[A-Za-z0-9\-]{1,10}}', 'SquawkController@deleteSquawkAssignment'); - + // Regional Pressure $router->get('regional-pressure', 'RegionalPressureController@getRegionalPressures'); + + // Plugin Errors + $router->post('plugin-error', 'PluginErrorController@recordPluginError'); }); // Routes for user administration @@ -111,13 +114,13 @@ // Routes for user administration $router->group(['middleware' => 'scopes:' . AuthServiceProvider::SCOPE_VERSION_ADMIN], function () use ($router) { - // A test route for useradmin access - $router->get('versionadmin', 'TeapotController@teapot'); + // A test route for useradmin access + $router->get('versionadmin', 'TeapotController@teapot'); - // Routes for returning information about versions - $router->get('version', 'VersionController@getAllVersions'); - $router->get('version/{version:[A-Za-z0-9\.\-]+}', 'VersionController@getVersion'); + // Routes for returning information about versions + $router->get('version', 'VersionController@getAllVersions'); + $router->get('version/{version:[A-Za-z0-9\.\-]+}', 'VersionController@getVersion'); - // Route for updating and creating versions - $router->put('version/{version:[A-Za-z0-9\.\-]+}', 'VersionController@createOrUpdateVersion'); + // Route for updating and creating versions + $router->put('version/{version:[A-Za-z0-9\.\-]+}', 'VersionController@createOrUpdateVersion'); }); diff --git a/tests/app/Events/PluginErrorReceivedEventTest.php b/tests/app/Events/PluginErrorReceivedEventTest.php new file mode 100644 index 000000000..136c0cc16 --- /dev/null +++ b/tests/app/Events/PluginErrorReceivedEventTest.php @@ -0,0 +1,21 @@ +assertInstanceOf(PluginErrorReceivedEvent::class, new PluginErrorReceivedEvent(new PluginError)); + } + + public function testItCanReturnTheError() + { + $error = new PluginError; + $event = new PluginErrorReceivedEvent($error); + $this->assertEquals($error, $event->getError()); + } +} diff --git a/tests/app/Http/Controllers/PluginErrorControllerTest.php b/tests/app/Http/Controllers/PluginErrorControllerTest.php new file mode 100644 index 000000000..afc71cdbd --- /dev/null +++ b/tests/app/Http/Controllers/PluginErrorControllerTest.php @@ -0,0 +1,103 @@ +assertInstanceOf(PluginErrorController::class, $this->app->make(PluginErrorController::class)); + } + + public function testItReturns400OnMissingUserReport() + { + $this->makeAuthenticatedApiRequest( + self::METHOD_POST, + 'plugin-error', + [ + 'data' => [ + 'foo' => 'bar' + ], + ] + )->seeStatusCode(400); + } + + public function testItReturns400OnMissingData() + { + $this->makeAuthenticatedApiRequest( + self::METHOD_POST, + 'plugin-error', + [ + 'user_report' => true, + ] + )->seeStatusCode(400); + } + + public function testItReturnsNoContentOnSuccess() + { + $this->makeAuthenticatedApiRequest( + self::METHOD_POST, + 'plugin-error', + [ + 'user_report' => true, + 'data' => [ + 'foo' => 'bar', + 'baz' => [ + 'foo' => 'bar', + ] + ], + ] + )->seeStatusCode(204); + } + + public function testItStoresUserReports() + { + $data = [ + 'baz' => [ + 'foo' => 'bar', + ], + 'foo' => 'bar', + ]; + + $this->makeAuthenticatedApiRequest( + self::METHOD_POST, + 'plugin-error', + [ + 'user_report' => true, + 'data' => $data + ] + ); + + $pluginError = PluginError::orderBy('created_at', 'desc')->first(); + $this->assertEquals(self::ACTIVE_USER_CID, $pluginError->user_id); + $this->assertEquals(1, $pluginError->user_report); + $this->assertEquals($data, json_decode($pluginError->data, true)); + } + + public function testItStoresNonUserReports() + { + $data = [ + 'baz' => [ + 'foo' => 'bar', + ], + 'foo' => 'bar', + ]; + + $this->makeAuthenticatedApiRequest( + self::METHOD_POST, + 'plugin-error', + [ + 'user_report' => false, + 'data' => $data + ] + ); + + $pluginError = PluginError::orderBy('created_at', 'desc')->first(); + $this->assertEquals(self::ACTIVE_USER_CID, $pluginError->user_id); + $this->assertEquals(0, $pluginError->user_report); + $this->assertEquals($data, json_decode($pluginError->data, true)); + } +} diff --git a/tests/app/Listeners/PluginError/RecordPluginErrorInBugsnagTest.php b/tests/app/Listeners/PluginError/RecordPluginErrorInBugsnagTest.php new file mode 100644 index 000000000..e3cf150d6 --- /dev/null +++ b/tests/app/Listeners/PluginError/RecordPluginErrorInBugsnagTest.php @@ -0,0 +1,22 @@ +assertInstanceOf(RecordPluginErrorInBugsnag::class, $listener); + } + + public function testItStopsEventPropagation() + { + $listener = new RecordPluginErrorInBugsnag(); + $this->assertFalse($listener->handle(new PluginErrorReceivedEvent(new PluginError))); + } +}