diff --git a/.github/workflows/laravel.yml b/.github/workflows/laravel.yml index b959cdd36..1eca79722 100644 --- a/.github/workflows/laravel.yml +++ b/.github/workflows/laravel.yml @@ -21,11 +21,14 @@ jobs: - name: Create DB run: | - touch /tmp/data.db php artisan migrate + env: + APP_ENV: testing - name: Generate key run: php artisan key:generate + env: + APP_ENV: testing - name: Passport install run: php .docker/passport-install.php . diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 0cdccacce..8f8516125 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -57,6 +57,7 @@ class Handler extends ExceptionHandler */ protected function shallWeStackTrace(Throwable $e): bool { + $x = config('app.env'); if (config('app.env') === 'production') { foreach ($this->dontStackTrace as $type) { if ($e instanceof $type) { @@ -117,15 +118,15 @@ public function render($request, Throwable $exception) ->guest('/login') // We anticipate expiry to be the most common reason. ->withErrors(['error_message' => trans('auth.expired')]) - ; + ; } if ($exception instanceof MethodNotAllowedHttpException) { // User somehow tried an HTTP verb a resource doesn't support // Send them somewhere safe. '' is the "/" equivalent. return redirect(''); - // For some reason ->withErrors() won't "take"; - // The session gets flashed, but it doesn't survive. + // For some reason ->withErrors() won't "take"; + // The session gets flashed, but it doesn't survive. } return parent::render($request, $exception); @@ -150,7 +151,7 @@ protected function unauthenticated($request, AuthenticationException $exception) case 'api': $login = route('api.login'); break; - //TODO: merge + //TODO: merge case 'store': $login = route('store.login'); break; diff --git a/app/Note.php b/app/Note.php index fc08145b3..b63ceffd5 100644 --- a/app/Note.php +++ b/app/Note.php @@ -4,6 +4,10 @@ use Illuminate\Database\Eloquent\Model; +/** + * @property Family $family; + * @property CentreUser $user; + */ class Note extends Model { /** diff --git a/app/Notifications/AdminPasswordResetNotification.php b/app/Notifications/AdminPasswordResetNotification.php index c15b1315c..6f003e52e 100644 --- a/app/Notifications/AdminPasswordResetNotification.php +++ b/app/Notifications/AdminPasswordResetNotification.php @@ -59,7 +59,10 @@ public function toMail($notifiable) return (new MailMessage) ->subject('Password Reset Request Notification') ->greeting('Hi '. $this->name .',') - ->line('You are receiving this email because we received a password reset request for your account on the Rosie admin service.') + ->line( + 'You are receiving this email because we received a password reset request for your account + on the Rosie admin service.' + ) ->action( 'Reset Password', ('http://' diff --git a/app/Wrappers/SecretStreamWrapper.php b/app/Wrappers/SecretStreamWrapper.php index f14e4669c..9ff44f733 100644 --- a/app/Wrappers/SecretStreamWrapper.php +++ b/app/Wrappers/SecretStreamWrapper.php @@ -55,10 +55,11 @@ class SecretStreamWrapper * We setup the resource we're wrapping and the cryptography we're going to rely on, deriving our key from * self::getKey(). * - * @param $path the full path provided to @see fopen() or similar - * @param $mode only "r" is supported - * @param $options some options encoded as bits - * @param $open + * @param $path string the full path provided to @see fopen() or similar + * @param $mode string only "r" is supported + * @param $options int some options encoded as bits + * @param $open mixed not used? + * * @return bool whether opening was successful */ public function stream_open($path, $mode, $options, &$open) { diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php index 2cd4ff0f9..66525f01c 100644 --- a/tests/CreatesApplication.php +++ b/tests/CreatesApplication.php @@ -4,6 +4,7 @@ use Hash; use Illuminate\Contracts\Console\Kernel; +use Illuminate\Contracts\Foundation\Application; trait CreatesApplication { diff --git a/tests/Unit/Console/KernelTest.php b/tests/Unit/Console/KernelTest.php index bc6b97783..e8398183a 100644 --- a/tests/Unit/Console/KernelTest.php +++ b/tests/Unit/Console/KernelTest.php @@ -4,7 +4,6 @@ use Illuminate\Console\Scheduling\Schedule; use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Foundation\Application; use Illuminate\Foundation\Testing\TestCase; use Tests\CreatesApplication; diff --git a/tests/Unit/Controllers/Service/Admin/DeliveriesControllerTest.php b/tests/Unit/Controllers/Service/Admin/DeliveriesControllerTest.php index 444f297a0..dce85be62 100644 --- a/tests/Unit/Controllers/Service/Admin/DeliveriesControllerTest.php +++ b/tests/Unit/Controllers/Service/Admin/DeliveriesControllerTest.php @@ -4,21 +4,22 @@ use App\AdminUser; use App\Centre; +use App\Voucher; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Foundation\Testing\DatabaseMigrations; +use Mockery; +use Mockery\MockInterface; use Tests\TestCase; class DeliveriesControllerTest extends TestCase { use DatabaseMigrations; - /** @var AdminUser $adminUser */ - private $adminUser; - - /** @var Centre $centre */ - private $centre; - - private $vouchersDeliveryroute; + private AdminUser $adminUser; + private Centre $centre; + private string $vouchersDeliveryroute; + private Collection $vouchers; public function setUp(): void { @@ -26,15 +27,21 @@ public function setUp(): void $this->adminUser = factory(AdminUser::class)->create(); $this->centre = factory(Centre::class)->create(); + $this->vouchers = factory(Voucher::class, 5)->state('printed')->create(); $this->vouchersDeliveryroute = route('admin.deliveries.store'); + + // TODO The vouchers created have random code lengths,is that right? + $shortcode = $this->vouchers[0]->sponsor->shortcode; + foreach ($this->vouchers as $index => $voucher) { + $voucher->code = $shortcode . sprintf("%04d", $index); + $voucher->save(); + } } /** * @test - * - * @return void */ - public function testStoreWithoutStartEndDateErrors() + public function testStoreWithoutStartEndDateErrors(): void { $this->actingAs($this->adminUser, 'admin') ->post($this->vouchersDeliveryroute, [ @@ -54,10 +61,8 @@ public function testStoreWithoutStartEndDateErrors() /** * @test - * - * @return void */ - public function testStoreStartEndSwapped() + public function testStoreStartEndSwapped(): void { $this->actingAs($this->adminUser, 'admin') ->post($this->vouchersDeliveryroute, [ @@ -75,10 +80,8 @@ public function testStoreStartEndSwapped() /** * @test - * - * @return void */ - public function testStoreCentreIsNotNumberErrors() + public function testStoreCentreIsNotNumberErrors(): void { $this->actingAs($this->adminUser, 'admin') ->post($this->vouchersDeliveryroute, [ @@ -94,10 +97,8 @@ public function testStoreCentreIsNotNumberErrors() /** * @test - * - * @return void */ - public function testStoreStartIsNotTheSameSponsorAsEndErrors() + public function testStoreStartIsNotTheSameSponsorAsEndErrors(): void { $this->actingAs($this->adminUser, 'admin') ->post($this->vouchersDeliveryroute, [ @@ -112,4 +113,27 @@ public function testStoreStartIsNotTheSameSponsorAsEndErrors() 'voucher-end' => 'The voucher-end field must be the same sponsor as the voucher-start field.' ]); } + + /** + * @test + */ + public function testStore(): void + { + $vouchers = Voucher::all()->sortBy("code"); + $firstVoucher = $vouchers->first(); + $lastVoucher = $vouchers->last(); + $response = $this->actingAs($this->adminUser, 'admin') + ->post($this->vouchersDeliveryroute, [ + 'centre' => $this->centre->id, + 'voucher-start' => $firstVoucher->code, + 'voucher-end' => $lastVoucher->code, + 'date-sent' => Carbon::now()->format('Y-m-d'), + ]) + ->assertStatus(302) + ->assertRedirectToRoute("admin.deliveries.create"); + + // TODO This needs loads more work, I can't get a deliverable range so the test never goes beyond here + // https://github.com/neontribe/ARCVService/blob/096bd1850c8a1a4baaf5e95d916a6ef6a062012f/app/Http/Controllers/Service/Admin/DeliveriesController.php#L63 + + } } diff --git a/tests/Unit/Controllers/Service/Auth/ForgotPasswordControllerTest.php b/tests/Unit/Controllers/Service/Auth/ForgotPasswordControllerTest.php new file mode 100644 index 000000000..4685d1011 --- /dev/null +++ b/tests/Unit/Controllers/Service/Auth/ForgotPasswordControllerTest.php @@ -0,0 +1,28 @@ +getMethod("broker"); + $method->setAccessible(true); + $result = $method->invoke($fpc); + $this->assertInstanceOf(PasswordBroker::class, $result); + } + public function testShowLinkRequestForm() + { + $fpc = new ForgotPasswordController(); + $result = $fpc->showLinkRequestForm(); + $this->assertInstanceOf(View::class, $result); + } +} diff --git a/tests/Unit/Controllers/Service/Auth/LoginControllerTest.php b/tests/Unit/Controllers/Service/Auth/LoginControllerTest.php new file mode 100644 index 000000000..f6617742d --- /dev/null +++ b/tests/Unit/Controllers/Service/Auth/LoginControllerTest.php @@ -0,0 +1,80 @@ +check")->once()->andReturn(false); + // Called in controller + Auth::shouldReceive("guard->attempt")->once()->andReturn(true); + + /** + * @var RedirectResponse $response + */ + $response = $this->post( + route('admin.login'), + [ + 'email' => 'twiki@example.com', + 'password' => 'bdbdbd', + ] + ); + $this->assertEquals(302, $response->status()); + $a = route('admin.dashboard'); + $b = $response->getTargetUrl(); + $this->assertEquals(route('admin.dashboard'), $response->getTargetUrl()); + } + + public function testLoginInvalidForm() + { + // Called in middleware + Auth::shouldReceive("guard->check")->once()->andReturn(false); + + /** + * @var RedirectResponse $response + */ + $response = $this->post( + route('admin.login'), + [ + 'fu' => 'bar' + ] + ); + $this->assertEquals(302, $response->status()); + $a = route('admin.dashboard'); + $b = $response->getTargetUrl(); + $this->assertEquals(route('admin.dashboard'), $response->getTargetUrl()); + } + + public function testLoginFail() + { + // Called in middleware + Auth::shouldReceive("guard->check")->once()->andReturn(false); + // Called in controller + Auth::shouldReceive("guard->attempt")->once()->andReturn(false); + + /** + * @var RedirectResponse $response + */ + $response = $this->post( + route('admin.login'), + [ + 'email' => 'princess.ardala@example.com', + 'password' => 'draconia', + ] + ); + $this->assertEquals(302, $response->status()); + $a = route('admin.dashboard'); + $b = $response->getTargetUrl(); + $this->assertEquals(route('admin.dashboard'), $response->getTargetUrl()); + } + +// public function testLoginTooManyLogins() +// { +// } +} diff --git a/tests/Unit/Controllers/Service/Auth/LoginRequestTest.php b/tests/Unit/Controllers/Service/Auth/LoginRequestTest.php new file mode 100644 index 000000000..2360217ee --- /dev/null +++ b/tests/Unit/Controllers/Service/Auth/LoginRequestTest.php @@ -0,0 +1,25 @@ +assertTrue($lr->authorize()); + } + + public function testRules() + { + $lr = new LoginRequest(); + $rules = $lr->rules(); + $this->assertArrayHasKey('username', $rules); + $this->assertArrayHasKey('password', $rules); + $this->assertEquals("required|email", $rules["username"]); + $this->assertEquals("required", $rules["password"]); + } +} diff --git a/tests/Unit/Controllers/Service/DashboardControllerTest.php b/tests/Unit/Controllers/Service/DashboardControllerTest.php new file mode 100644 index 000000000..d03333e43 --- /dev/null +++ b/tests/Unit/Controllers/Service/DashboardControllerTest.php @@ -0,0 +1,21 @@ +get(route('????'))->assertStatus(200); + + $dc = new DashboardController(); + $response = $dc->index(); + $this->assertInstanceOf(View::class, $response); + } +} diff --git a/tests/Unit/Controllers/Service/VersionControllerTest.php b/tests/Unit/Controllers/Service/VersionControllerTest.php new file mode 100644 index 000000000..e447c24d1 --- /dev/null +++ b/tests/Unit/Controllers/Service/VersionControllerTest.php @@ -0,0 +1,15 @@ +get(route('version'))->assertStatus(200); + $json = $response->json(); + $this->assertArrayHasKey("Service/API", $json); + } +} diff --git a/tests/Unit/Exceptions/HandlerTest.php b/tests/Unit/Exceptions/HandlerTest.php new file mode 100644 index 000000000..4114ede1a --- /dev/null +++ b/tests/Unit/Exceptions/HandlerTest.php @@ -0,0 +1,56 @@ +app); + $handler->report($rte); + + // test is production + config(['app.env' => 'production']); + $app = $this->createApplication(); + $hre = new HttpResponseException($response); + config(['app.env' => 'production']); + \Log::expects("error")->once(); + $handler = new Handler($app); + $handler->report($hre); + } + + public function testRender() + { + $handler = new Handler($this->app); + + $request = new Request(); + $response = $handler->render($request, new RuntimeException()); + $this->assertInstanceOf(Response::class, $response); + + $response = $handler->render($request, new TokenMismatchException()); + $this->assertInstanceOf(RedirectResponse::class, $response); + + $allowed = []; + $response = $handler->render($request, new MethodNotAllowedHttpException($allowed)); + $this->assertInstanceOf(RedirectResponse::class, $response); + } +} diff --git a/tests/Unit/Models/AdminUserModelTest.php b/tests/Unit/Models/AdminUserModelTest.php new file mode 100644 index 000000000..e14073445 --- /dev/null +++ b/tests/Unit/Models/AdminUserModelTest.php @@ -0,0 +1,21 @@ +create(); + $adminUsers[0]->sendPasswordResetNotification("A1B2C3"); + Notification::assertCount(1); + } +} diff --git a/tests/Unit/Models/NoteModelTest.php b/tests/Unit/Models/NoteModelTest.php new file mode 100644 index 000000000..44cc0bd04 --- /dev/null +++ b/tests/Unit/Models/NoteModelTest.php @@ -0,0 +1,28 @@ +create()->first(); + + $note = new Note(); + $family->notes->add($note); + $family->save(); + + $notes = $family->notes; + $this->assertEquals(1, $notes->count()); + } +} diff --git a/tests/Unit/Notifications/AdminPasswordResetNotificationTest.php b/tests/Unit/Notifications/AdminPasswordResetNotificationTest.php new file mode 100644 index 000000000..fe079f769 --- /dev/null +++ b/tests/Unit/Notifications/AdminPasswordResetNotificationTest.php @@ -0,0 +1,49 @@ +token, $this->name); + $result = $aprn->toMail($notifiable); + + $this->assertInstanceOf(MailMessage::class, $result); + } + + public function testToArray() + { + $notifiable = new Mock(); + + $aprn = new AdminPasswordResetNotification($this->token, $this->name); + $result = $aprn->toArray($notifiable); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + public function testVia() + { + $notifiable = new Mock(); + + $aprn = new AdminPasswordResetNotification($this->token, $this->name); + $result = $aprn->via($notifiable); + + $this->assertIsArray($result); + $this->assertEquals(['mail'], $result); + } +} diff --git a/tests/Unit/Notifications/ApiPasswordResetNotificationTest.php b/tests/Unit/Notifications/ApiPasswordResetNotificationTest.php new file mode 100644 index 000000000..f10e151ab --- /dev/null +++ b/tests/Unit/Notifications/ApiPasswordResetNotificationTest.php @@ -0,0 +1,65 @@ +token, $this->name); + $result = $aprn->toMail($notifiable); + + $this->assertInstanceOf(MailMessage::class, $result); + } + + public function testToArray() + { + $notifiable = new Mock(); + + $aprn = new ApiPasswordResetNotification($this->token, $this->name); + $result = $aprn->toArray($notifiable); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + public function testVia() + { + $notifiable = new Mock(); + + $aprn = new ApiPasswordResetNotification($this->token, $this->name); + $result = $aprn->via($notifiable); + + $this->assertIsArray($result); + $this->assertEquals(['mail'], $result); + } +} + +class MockNotification +{ + private string $email; + + public function __construct(string $email) + { + $this->email = $email; + } + + public function getEmailForPasswordReset(): string + { + return $this->email; + } +}