From b62fa7757cc7525b0ef8368e251835ac51816ef4 Mon Sep 17 00:00:00 2001 From: Nabeel Shahzad Date: Thu, 4 Jan 2018 19:33:23 -0600 Subject: [PATCH] Overhaul of ACARS/PIREP APIs --- app/Exceptions/Handler.php | 3 +- app/Http/Controllers/Api/PirepController.php | 221 ++++++++++++++---- app/Http/Controllers/Api/RestController.php | 89 +++++++ app/Http/Controllers/AppBaseController.php | 9 - app/Models/Pirep.php | 2 +- app/Routes/api.php | 8 +- .../views/layouts/default/acars/map.blade.php | 2 +- tests/AcarsTest.php | 209 +++++++++++++++-- tests/PIREPTest.php | 11 +- tests/TestCase.php | 23 +- 10 files changed, 491 insertions(+), 86 deletions(-) create mode 100644 app/Http/Controllers/Api/RestController.php diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index b944e8fd..5a848287 100755 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -68,7 +68,8 @@ class Handler extends ExceptionHandler 'error' => [ 'code' => $exception->getCode() , 'http_code' => $http_code, - 'message' => $exception->getMessage() + 'message' => $exception->getMessage(), + 'trace' => $exception->getTrace()[0], ] ], $status); } diff --git a/app/Http/Controllers/Api/PirepController.php b/app/Http/Controllers/Api/PirepController.php index 60885e0c..be702705 100644 --- a/app/Http/Controllers/Api/PirepController.php +++ b/app/Http/Controllers/Api/PirepController.php @@ -21,18 +21,29 @@ use App\Repositories\PirepRepository; use App\Http\Resources\Acars as AcarsResource; use App\Http\Resources\Pirep as PirepResource; +use App\Http\Resources\AcarsLog as AcarsLogResource; +use App\Http\Resources\AcarsRoute as AcarsRouteResource; -use App\Http\Controllers\AppBaseController; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -class PirepController extends AppBaseController +class PirepController extends RestController { - protected $acarsRepo, - $geoSvc, - $pirepRepo, - $pirepSvc; + public static $acars_rules = [ + 'altitude', + 'level', + 'heading', + 'vs', + 'gs', + 'transponder', + 'autopilot', + 'fuel_flow', + 'log', + 'lat', + 'lon', + 'created_at', + ]; - protected $check_attrs = [ + public static $pirep_rules = [ 'airline_id', 'aircraft_id', 'dpt_airport_id', @@ -48,6 +59,18 @@ class PirepController extends AppBaseController 'notes', ]; + protected $acarsRepo, + $geoSvc, + $pirepRepo, + $pirepSvc; + + /** + * PirepController constructor. + * @param AcarsRepository $acarsRepo + * @param GeoService $geoSvc + * @param PirepRepository $pirepRepo + * @param PIREPService $pirepSvc + */ public function __construct( AcarsRepository $acarsRepo, GeoService $geoSvc, @@ -72,22 +95,17 @@ class PirepController extends AppBaseController * status, and whatever other statuses may be defined * * TODO: Allow extra fields, etc to be set. Aircraft, etc + * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException */ public function prefile(Request $request) { Log::info('PIREP Prefile, user '.Auth::user()->id, $request->toArray()); - $attrs = [ - 'user_id' => Auth::user()->id, - 'state' => PirepState::IN_PROGRESS, - 'status' => PirepStatus::PREFILE, - ]; - - foreach ($this->check_attrs as $attr) { - if ($request->filled($attr)) { - $attrs[$attr] = $request->get($attr); - } - } + $attrs = $this->getFromReq($request, self::$pirep_rules, [ + 'user_id' => Auth::user()->id, + 'state' => PirepState::IN_PROGRESS, + 'status' => PirepStatus::PREFILE, + ]); $pirep = new Pirep($attrs); @@ -130,16 +148,10 @@ class PirepController extends AppBaseController throw new BadRequestHttpException('PIREP has been cancelled, updates can\'t be posted'); } - $attrs = [ - 'state' => PirepState::PENDING, - 'status' => PirepStatus::ARRIVED, - ]; - - foreach($this->check_attrs as $attr) { - if($request->filled($attr)) { - $attrs[$attr] = $request->get($attr); - } - } + $attrs = $this->getFromReq($request, self::$pirep_rules, [ + 'state' => PirepState::PENDING, + 'status' => PirepStatus::ARRIVED, + ]); $pirep_fields = []; if($request->filled('fields')) { @@ -187,7 +199,7 @@ class PirepController extends AppBaseController * @param Request $request * @return \Illuminate\Contracts\Routing\ResponseFactory */ - public function acars_get($id, Request $request) + public function acars_geojson($id, Request $request) { $pirep = $this->pirepRepo->find($id); $geodata = $this->geoSvc->getFeatureFromAcars($pirep); @@ -197,11 +209,28 @@ class PirepController extends AppBaseController ]); } + /** + * Return the GeoJSON for the ACARS line + * @param $id + * @param Request $request + * @return AcarsRouteResource + */ + public function acars_get($id, Request $request) + { + $pirep = $this->pirepRepo->find($id); + + AcarsRouteResource::withoutWrapping(); + return new AcarsRouteResource(Acars::where([ + 'pirep_id' => $id, + 'type' => AcarsType::FLIGHT_PATH + ])->orderBy('created_at', 'asc')->get()); + } + /** * Post ACARS updates for a PIREP * @param $id * @param Request $request - * @return AcarsResource + * @return AcarsRouteResource * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException */ public function acars_store($id, Request $request) @@ -214,20 +243,31 @@ class PirepController extends AppBaseController } Log::info('Posting ACARS update', $request->toArray()); - $attrs = $request->toArray(); - $attrs['pirep_id'] = $id; - $attrs['type'] = AcarsType::FLIGHT_PATH; + $this->validate($request, ['positions' => 'required']); + $positions = $request->post()['positions']; - $update = Acars::create($attrs); - $update->save(); + foreach($positions as $position) + { + try { + $attrs = $this->getFromReq( + $position, + self::$acars_rules, + ['pirep_id' => $id, 'type' => AcarsType::FLIGHT_PATH] + ); + + $update = Acars::create($attrs); + $update->save(); + } catch (\Exception $e) { + Log::error($e); + } + } # Change the PIREP status $pirep->status = PirepStatus::ENROUTE; $pirep->save(); - AcarsResource::withoutWrapping(); - return new AcarsResource($update); + return $this->acars_get($id, $request); } /** @@ -235,7 +275,7 @@ class PirepController extends AppBaseController * But rather in a log file. * @param $id * @param Request $request - * @return AcarsResource + * @return AcarsLogResource * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException */ public function acars_log($id, Request $request) @@ -247,20 +287,103 @@ class PirepController extends AppBaseController throw new BadRequestHttpException('PIREP has been cancelled, updates can\'t be posted'); } - Log::info('Posting ACARS update', $request->toArray()); - $attrs = $request->toArray(); + Log::info('Posting ACARS log', $request->toArray()); - $attrs['pirep_id'] = $id; - $attrs['type'] = AcarsType::LOG; + $attrs = $this->getFromReq($request, [ + 'log' => 'required', + 'lat' => 'nullable', + 'lon' => 'nullable', + ], ['pirep_id' => $id, 'type' => AcarsType::LOG]); - $update = Acars::create($attrs); - $update->save(); + $acars = Acars::create($attrs); + $acars->save(); - # Change the PIREP status - $pirep->status = PirepStatus::ENROUTE; - $pirep->save(); + AcarsLogResource::withoutWrapping(); + return new AcarsLogResource($acars); + } - AcarsResource::withoutWrapping(); - return new AcarsResource($update); + /** + * @param $id + * @param Request $request + * @return AcarsRouteResource + */ + public function route_get($id, Request $request) + { + $this->pirepRepo->find($id); + + AcarsRouteResource::withoutWrapping(); + return new AcarsRouteResource(Acars::where([ + 'pirep_id' => $id, + 'type' => AcarsType::ROUTE + ])->orderBy('order', 'asc')->get()); + } + + /** + * Post the ROUTE for a PIREP, can be done from the ACARS log + * @param $id + * @param Request $request + * @return AcarsRouteResource + * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + */ + public function route_post($id, Request $request) + { + $pirep = $this->pirepRepo->find($id); + + # Check if the status is cancelled... + if ($pirep->state === PirepState::CANCELLED) { + throw new BadRequestHttpException('PIREP has been cancelled, updates can\'t be posted'); + } + + Log::info('Posting ACARS ROUTE', $request->toArray()); + + $this->validate($request, [ + 'route.*.name' => 'required', + 'route.*.order' => 'required|int', + 'route.*.nav_type' => 'nullable|int', + 'route.*.lat' => 'required|numeric', + 'route.*.lon' => 'required|numeric', + ]); + + $route = $request->all()['route']; + foreach($route as $position) { + $attrs = [ + 'pirep_id' => $id, + 'type' => AcarsType::ROUTE, + 'name' => $position['name'], + 'order' => $position['order'], + 'lat' => $position['lat'], + 'lon' => $position['lon'], + ]; + + if(array_key_exists('nav_type', $position)) { + $attrs['nav_type'] = $position['nav_type']; + } + + try { + $acars = Acars::create($attrs); + $acars->save(); + } catch (\Exception $e) { + Log::error($e); + } + } + + return $this->route_get($id, $request); + } + + /** + * @param $id + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function route_delete($id, Request $request) + { + $this->pirepRepo->find($id); + + Acars::where([ + 'pirep_id' => $id, + 'type' => AcarsType::ROUTE + ])->delete(); + + return $this->message('Route deleted'); } } diff --git a/app/Http/Controllers/Api/RestController.php b/app/Http/Controllers/Api/RestController.php new file mode 100644 index 00000000..9e4c2189 --- /dev/null +++ b/app/Http/Controllers/Api/RestController.php @@ -0,0 +1,89 @@ + 0) { + $is_validation = true; + } + + if($is_validation) { + $this->validate($request, $attrs_or_validations); + } + + $fields = []; + foreach($attrs_or_validations as $idx => $field) { + if($is_validation) { + $field = $idx; + } + + if($request instanceof Request) { + if ($request->filled($field)) { + $fields[$field] = $request->get($field); + } + } else { + if(array_key_exists($field, $request)) { + $fields[$field] = $request[$field]; + } + } + } + + if(!empty($addtl_fields) && \is_array($addtl_fields)) { + $fields = array_merge($fields, $addtl_fields); + } + + return $fields; + } + + /** + * Run a validation + * @param $request + * @param $rules + * @return bool + * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + */ + public function validate(Request $request, $rules) + { + $validator = Validator::make($request->all(), $rules); + if (!$validator->passes()) { + throw new BadRequestHttpException($validator->errors(), null, 400); + } + + return true; + } + + /** + * Simple normalized method for forming the JSON responses + * @param $message + * @return \Illuminate\Http\JsonResponse + */ + public function message($message) + { + return response()->json([ + 'message' => $message + ]); + } +} diff --git a/app/Http/Controllers/AppBaseController.php b/app/Http/Controllers/AppBaseController.php index 92b49ebd..b4673979 100644 --- a/app/Http/Controllers/AppBaseController.php +++ b/app/Http/Controllers/AppBaseController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers; -use InfyOm\Generator\Utils\ResponseUtil; use Response; /** @@ -18,13 +17,5 @@ use Response; */ class AppBaseController extends Controller { - public function sendResponse($result, $message) - { - return Response::json(ResponseUtil::makeResponse($message, $result)); - } - public function sendError($error, $code = 404) - { - return Response::json(ResponseUtil::makeError($error), $code); - } } diff --git a/app/Models/Pirep.php b/app/Models/Pirep.php index d9f8a267..18d87b64 100644 --- a/app/Models/Pirep.php +++ b/app/Models/Pirep.php @@ -104,7 +104,7 @@ class Pirep extends BaseModel { return $this->hasMany('App\Models\Acars', 'pirep_id') ->where('type', AcarsType::ROUTE) - ->orderBy('created_at', 'asc'); + ->orderBy('order', 'asc'); } public function aircraft() diff --git a/app/Routes/api.php b/app/Routes/api.php index 135b677b..8790730b 100755 --- a/app/Routes/api.php +++ b/app/Routes/api.php @@ -29,10 +29,16 @@ Route::group(['middleware' => ['api.auth']], function () Route::post('pireps/prefile', 'PirepController@prefile'); Route::post('pireps/{id}/file', 'PirepController@file'); Route::post('pireps/{id}/cancel', 'PirepController@cancel'); - Route::post('pireps/{id}/acars', 'PirepController@acars_store'); + + Route::get('pireps/{id}/acars/geojson', 'PirepController@acars_geojson'); + Route::get('pireps/{id}/acars/position', 'PirepController@acars_get'); Route::post('pireps/{id}/acars/position', 'PirepController@acars_store'); Route::post('pireps/{id}/acars/log', 'PirepController@acars_log'); + Route::get('pireps/{id}/route', 'PirepController@route_get'); + Route::post('pireps/{id}/route', 'PirepController@route_post'); + Route::delete('pireps/{id}/route', 'PirepController@route_delete'); + # This is the info of the user whose token is in use Route::get('user', 'UserController@index'); Route::get('users/{id}', 'UserController@get'); diff --git a/resources/views/layouts/default/acars/map.blade.php b/resources/views/layouts/default/acars/map.blade.php index 8d2274dd..b8e6ce67 100644 --- a/resources/views/layouts/default/acars/map.blade.php +++ b/resources/views/layouts/default/acars/map.blade.php @@ -13,7 +13,7 @@ diff --git a/tests/AcarsTest.php b/tests/AcarsTest.php index c579c5f2..e9757c6a 100644 --- a/tests/AcarsTest.php +++ b/tests/AcarsTest.php @@ -14,6 +14,33 @@ class AcarsTest extends TestCase $this->addData('base'); } + /** + * @param $route + * @param $points + * @param array $addtl_fields + */ + protected function allPointsInRoute($route, $points, $addtl_fields=[]) + { + if(empty($addtl_fields)) { + $addtl_fields = []; + } + + $fields = array_merge([ + 'name', + 'order', + 'lat', + 'lon' + ], $addtl_fields); + + $this->assertEquals(\count($route), \count($points)); + foreach($route as $idx => $point) { + //$this->assertHasKeys($points[$idx], $fields); + foreach($fields as $f) { + $this->assertEquals($point[$f], $points[$idx][$f]); + } + } + } + protected function getPirep($pirep_id) { $resp = $this->withHeaders($this->apiHeaders()) @@ -55,30 +82,73 @@ class AcarsTest extends TestCase $this->assertEquals(PirepState::IN_PROGRESS, $pirep['state']); $this->assertEquals(PirepStatus::PREFILE, $pirep['status']); + $uri = '/api/pireps/' . $pirep_id . '/acars/position'; + + # Test missing positions field + # Post an ACARS update + $update = []; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $update); + $response->assertStatus(400); + # Post an ACARS update - $uri = '/api/pireps/' . $pirep_id . '/acars'; $acars = factory(App\Models\Acars::class)->make()->toArray(); - $response = $this->withHeaders($this->apiHeaders())->post($uri, $acars); - $response->assertStatus(201); + unset($acars['id']); + $update = ['positions' => [$acars]]; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $update); + $response->assertStatus(200); $body = $response->json(); - $this->assertNotNull($body['id']); - $this->assertEquals($pirep_id, $body['pirep_id']); + $this->assertNotNull($body[0]['id']); + $this->assertEquals($pirep_id, $body[0]['pirep_id']); + //$this->assertHasKeys($body, $this->fillableFields(new \App\Models\Acars)); # Make sure PIREP state moved into ENROUTE $pirep = $this->getPirep($pirep_id); $this->assertEquals(PirepState::IN_PROGRESS, $pirep['state']); $this->assertEquals(PirepStatus::ENROUTE, $pirep['status']); - $uri = '/api/pireps/' . $pirep_id . '/acars'; $response = $this->withHeaders($this->apiHeaders())->get($uri); $response->assertStatus(200); + $body = $response->json(); - /*$body = $response->json(); - $this->assertEquals(1, $this->count($body)); - $this->assertEquals($pirep_id, $body[0]['pirep_id']);*/ + $this->assertNotNull($body); + $this->assertCount(1, $body); + $this->assertEquals($acars['lat'], $body[0]['lat']); + $this->assertEquals($acars['lon'], $body[0]['lon']); + + //$this->allPointsInRoute([$acars], $body); } + /** + * Test publishing multiple, batched updates + */ + public function testMultipleAcarsPositionUpdates() + { + $pirep = factory(App\Models\Pirep::class)->make()->toArray(); + + $uri = '/api/pireps/prefile'; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep); + $response->assertStatus(201); + + $pirep_id = $response->json()['id']; + + $uri = '/api/pireps/' . $pirep_id . '/acars/position'; + + # Post an ACARS update + $acars_count = \random_int(5, 50); + $acars = factory(App\Models\Acars::class, $acars_count)->make(['id'=>''])->toArray(); + + $update = ['positions' => $acars]; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $update); + $response->assertStatus(200)->assertJsonCount($acars_count); + + $response = $this->withHeaders($this->apiHeaders())->get($uri); + $response->assertStatus(200)->assertJsonCount($acars_count); + } + + /** + * + */ public function testNonExistentPirepGet() { $uri = '/api/pireps/DOESNTEXIST/acars'; @@ -86,35 +156,136 @@ class AcarsTest extends TestCase $response->assertStatus(404); } + /** + * + */ public function testNonExistentPirepStore() { - $uri = '/api/pireps/DOESNTEXIST/acars'; + $uri = '/api/pireps/DOESNTEXIST/acars/position'; $acars = factory(App\Models\Acars::class)->make()->toArray(); $response = $this->withHeaders($this->apiHeaders())->post($uri, $acars); $response->assertStatus(404); } + /** + * + */ public function testAcarsIsoDate() { $pirep = factory(App\Models\Pirep::class)->make()->toArray(); + $uri = '/api/pireps/prefile'; $response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep); $pirep_id = $response->json()['id']; $dt = date('c'); - $uri = '/api/pireps/' . $pirep_id . '/acars'; - $acars = factory(App\Models\Acars::class)->make()->toArray(); - $acars['sim_time'] = $dt; + $uri = '/api/pireps/' . $pirep_id . '/acars/position'; + $acars = factory(App\Models\Acars::class)->make([ + 'sim_time' => $dt + ])->toArray(); - $response = $this->withHeaders($this->apiHeaders())->post($uri, $acars); - $response->assertStatus(201); - - /*$uri = '/api/pireps/' . $pirep_id . '/acars'; - $response = $this->withHeaders($this->apiHeaders())->get($uri); + $update = ['positions' => [$acars]]; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $update); $response->assertStatus(200); + } + + /** + * Test the validation + */ + public function testAcarsInvalidRoutePost() + { + $pirep = factory(App\Models\Pirep::class)->make()->toArray(); + + $uri = '/api/pireps/prefile'; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep); + $pirep_id = $response->json()['id']; + + $post_route = ['order' => 1, 'name' => 'NAVPOINT']; + $uri = '/api/pireps/' . $pirep_id . '/route'; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $post_route); + $response->assertStatus(400); + + $post_route = [ + ['order' => 1, 'name' => 'NAVPOINT', 'lat' => 'notanumber', 'lon' => 34.11] + ]; + + $uri = '/api/pireps/' . $pirep_id . '/route'; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $post_route); + $response->assertStatus(400); + } + + public function testAcarsLogPost() + { + $pirep = factory(App\Models\Pirep::class)->make()->toArray(); + + $uri = '/api/pireps/prefile'; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep); + $pirep_id = $response->json()['id']; + + $acars = factory(App\Models\Acars::class)->make(); + $post_log = [ + 'log' => $acars->log + ]; + + $uri = '/api/pireps/' . $pirep_id . '/acars/log'; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $post_log); + $response->assertStatus(201); + } + + /** + * + */ + public function testAcarsRoutePost() + { + $pirep = factory(App\Models\Pirep::class)->make()->toArray(); + + $uri = '/api/pireps/prefile'; + $response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep); + $pirep_id = $response->json()['id']; + + $order = 1; + $post_route = []; + $route_count = \random_int(5, 50); + + $route = factory(App\Models\Navdata::class, $route_count)->create(); + foreach($route as $position) { + $post_route[] = [ + 'order' => $order, + 'name' => $position->name, + 'lat' => $position->lat, + 'lon' => $position->lon, + ]; + + ++$order; + } + + $uri = '/api/pireps/'.$pirep_id.'/route'; + $response = $this->withHeaders($this->apiHeaders())->post($uri, ['route' => $post_route]); + $response->assertStatus(200)->assertJsonCount($route_count); $body = $response->json(); - $this->assertEquals($dt, $body[0]['sim_time']);*/ + $this->allPointsInRoute($post_route, $body); + + /** + * Get + */ + + $uri = '/api/pireps/' . $pirep_id . '/route'; + $response = $this->withHeaders($this->apiHeaders())->get($uri); + $response->assertStatus(200)->assertJsonCount($route_count); + $body = $response->json(); + $this->allPointsInRoute($post_route, $body); + + /** + * Delete and then recheck + */ + $uri = '/api/pireps/' . $pirep_id . '/route'; + $response = $this->withHeaders($this->apiHeaders())->delete($uri); + $response->assertStatus(200); + + $uri = '/api/pireps/' . $pirep_id . '/route'; + $response = $this->withHeaders($this->apiHeaders())->get($uri); + $response->assertStatus(200)->assertJsonCount(0); } /** diff --git a/tests/PIREPTest.php b/tests/PIREPTest.php index d11c6908..88abd10f 100644 --- a/tests/PIREPTest.php +++ b/tests/PIREPTest.php @@ -193,10 +193,13 @@ class PIREPTest extends TestCase $response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep); $pirep_id = $response->json()['id']; - $uri = '/api/pireps/' . $pirep_id . '/acars'; + $uri = '/api/pireps/' . $pirep_id . '/acars/position'; $acars = factory(App\Models\Acars::class)->make()->toArray(); - $response = $this->withHeaders($this->apiHeaders())->post($uri, $acars); - $response->assertStatus(201); + $response = $this->withHeaders($this->apiHeaders())->post($uri, [ + 'positions' => [$acars] + ]); + + $response->assertStatus(200); # Cancel it $uri = '/api/pireps/' . $pirep_id . '/cancel'; @@ -204,7 +207,7 @@ class PIREPTest extends TestCase $response->assertStatus(200); # Should get a 400 when posting an ACARS update - $uri = '/api/pireps/' . $pirep_id . '/acars'; + $uri = '/api/pireps/' . $pirep_id . '/acars/position'; $acars = factory(App\Models\Acars::class)->make()->toArray(); $response = $this->withHeaders($this->apiHeaders())->post($uri, $acars); $response->assertStatus(400); diff --git a/tests/TestCase.php b/tests/TestCase.php index cea732ca..12051001 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -71,6 +71,27 @@ abstract class TestCase extends Illuminate\Foundation\Testing\TestCase { $svc = app('\App\Services\DatabaseService'); $file_path = base_path('tests/data/' . $file . '.yml'); - $svc->seed_from_yaml_file($file_path); + try { + $svc->seed_from_yaml_file($file_path); + } catch (Exception $e) { + } + } + + public function fillableFields(\Illuminate\Database\Eloquent\Model $model) + { + //$klass = new $model(); + return $model->fillable; + } + + /** + * Make sure an object has the list of keys + * @param $obj + * @param array $keys + */ + public function assertHasKeys($obj, $keys=[]) + { + foreach($keys as $key) { + $this->assertArrayHasKey($key, $obj); + } } }