diff --git a/phpunit.xml b/phpunit.xml index 0949fa0..beaa135 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,6 +4,9 @@ src/ + + src/EnumServiceProvider.php + @@ -15,4 +18,9 @@ ./tests/ + + + + + diff --git a/src/BaseEnumRoutable.php b/src/BaseEnumRoutable.php index afb39be..d618b10 100644 --- a/src/BaseEnumRoutable.php +++ b/src/BaseEnumRoutable.php @@ -21,7 +21,9 @@ abstract class BaseEnumRoutable extends BaseEnum implements UrlRoutable */ public function getRouteKey() { + // @codeCoverageIgnoreStart throw new NotImplementedException('getRouteKey not implemented, should not be used.'); + // @codeCoverageIgnoreEnd } /** @@ -31,7 +33,9 @@ abstract class BaseEnumRoutable extends BaseEnum implements UrlRoutable */ public function getRouteKeyName() { + // @codeCoverageIgnoreStart throw new NotImplementedException('getRouteKeyName not implemented, should not be used.'); + // @codeCoverageIgnoreEnd } /** @@ -72,6 +76,8 @@ abstract class BaseEnumRoutable extends BaseEnum implements UrlRoutable */ public function resolveChildRouteBinding($childType, $value, $field) { + // @codeCoverageIgnoreStart throw new NotImplementedException('resolveChildRouteBinding not implemented, should not be used.'); + // @codeCoverageIgnoreEnd } } diff --git a/tests/BaseEnumRoutableTest.php b/tests/BaseEnumRoutableTest.php new file mode 100644 index 0000000..9d353ed --- /dev/null +++ b/tests/BaseEnumRoutableTest.php @@ -0,0 +1,247 @@ +alias(Alarm::class, 'Alarm'); + $app->alias(Journal::class, 'Journal'); + $app->alias(Report::class, 'Report'); + $app->alias(Days::class, 'Days'); + $app->alias(ContactFormCivility::class, 'ContactFormCivility'); + $app->alias(ForceStringsFromInteger::class, 'ForceStringsFromInteger'); + $app->alias(NotRoutableEnum::class, 'NotRoutableEnum'); + } + + /** + * Get package providers. + * + * @param \Illuminate\Foundation\Application $app + * + * @return array + */ + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass + protected function getPackageProviders($app) + { + return [ + 'Kwaadpepper\Enum\EnumServiceProvider', + ]; + } + + + /** + * Defines migrations + * + * @return void + */ + protected function defineDatabaseMigrations() + { + Schema::create('reports', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->smallInteger('day'); + }); + Schema::create('alarms', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->smallInteger('day'); + }); + Schema::create('journals', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->smallInteger('day'); + }); + } + + /** + * Defines seeders + * + * @return void + */ + protected function defineDatabaseSeeders() + { + Report::create(['day' => Days::none()]); + Report::create(['day' => Days::mon()]); + Report::create(['day' => Days::tue()]); + Report::create(['day' => Days::wed()]); + Report::create(['day' => Days::thu()]); + Report::create(['day' => Days::fri()]); + Report::create(['day' => Days::sat()]); + Report::create(['day' => Days::sun()]); + Alarm::create(['day' => Days::none()]); + Alarm::create(['day' => Days::mon()]); + Alarm::create(['day' => Days::tue()]); + Alarm::create(['day' => Days::wed()]); + Alarm::create(['day' => Days::thu()]); + Alarm::create(['day' => Days::fri()]); + Alarm::create(['day' => Days::sat()]); + Alarm::create(['day' => Days::sun()]); + Journal::create(['day' => Days::none()]); + Journal::create(['day' => Days::mon()]); + Journal::create(['day' => Days::tue()]); + Journal::create(['day' => Days::wed()]); + Journal::create(['day' => Days::thu()]); + Journal::create(['day' => Days::fri()]); + Journal::create(['day' => Days::sat()]); + Journal::create(['day' => Days::sun()]); + } + + /** + * Define routes setup. + * + * @param \Illuminate\Routing\Router $router + * + * @return void + */ + protected function defineRoutes($router) + { + $router->get('/journals/{journal}', function (Journal $journal) { + return response()->json([$journal->day]); + })->middleware('bindings'); + $router->get('/reports/{report}', function (Report $report) { + return response()->json([$report->day]); + })->middleware('bindings'); + $router->get('/alarms/{alarm}', function (Alarm $alarm) { + return response()->json([$alarm->day]); + })->middleware('bindings'); + $router->get('/days/{day}', function (Days $day) { + return response()->json([$day]); + })->middleware('bindings'); + $router->get('/day-validation', function (PassDayRequest $request) { + return response()->json([Days::make((int)$request->day)]); + })->middleware('bindings'); + $router->get('/civilities/{civility}', function (ContactFormCivility $civility) { + return response()->json([$civility]); + })->middleware('bindings'); + $router->get('/force/{force}', function (ForceStringsFromInteger $force) { + return response()->json([$force]); + })->middleware('bindings'); + $router->get('/notroutable/{notroutable}', function (NotRoutableEnum $notroutable) { + return response()->json([$notroutable]); + })->middleware('bindings'); + } + + public function testRoutesDays() + { + $response = $this->call('GET', sprintf('/days/%s', Days::none()->value)); + $response->assertOk(); + $response->assertExactJson([Days::none()]); + $response = $this->call('GET', sprintf('/days/%s', Days::mon()->value)); + $response->assertOk(); + $response->assertExactJson([Days::mon()]); + } + + public function testRoutesCivilities() + { + $response = $this->call('GET', sprintf('/civilities/%s', ContactFormCivility::mme()->value)); + $response->assertOk(); + $response->assertExactJson([ContactFormCivility::mme()]); + } + + public function testRoutesWithEnumIntString() + { + $response = $this->call('GET', sprintf('/force/%s', ForceStringsFromInteger::valueB()->value)); + $response->assertOk(); + $response->assertExactJson([ForceStringsFromInteger::valueB()]); + } + + public function testRoutesWithEnumNotFound() + { + $response = $this->call('GET', '/force/6'); + $response->assertNotFound(); + } + + public function testRoutesWithEnumAsPrimaryKey() + { + $response = $this->call('GET', sprintf('/reports/%s', Days::none()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/reports/%s', Days::mon()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/reports/%s', Days::tue()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/reports/%s', Days::fri()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/reports/%s', 9999)); + $response->assertNotFound(); + } + + public function testRoutesWithEnumAsPrimaryKeyWithoutCastEnums() + { + $response = $this->call('GET', sprintf('/alarms/%s', Days::none()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/alarms/%s', Days::mon()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/alarms/%s', Days::tue()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/alarms/%s', Days::fri()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/alarms/%s', 9999)); + $response->assertNotFound(); + } + + public function testRoutesWithEnumAsPrimaryKeyWithoutCastEnumsAndCast() + { + $response = $this->call('GET', sprintf('/journals/%s', Days::none()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/journals/%s', Days::mon()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/journals/%s', Days::tue()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/journals/%s', Days::fri()->value)); + $response->assertOk(); + $response = $this->call('GET', sprintf('/journals/%s', 9999)); + $response->assertNotFound(); + } + + public function testRules() + { + $rule = new EnumIsValidRule(Days::class); + $this->assertTrue($rule->passes('day', Days::mon()->value)); + $this->assertFalse($rule->passes('day', 9999)); + $this->assertFalse($rule->passes('day', Days::mon())); + } + + public function testRulesRequest() + { + $response = $this->call('GET', sprintf('/day-validation?day=%s', Days::fri()->value)); + $response->assertOk(); + } + + public function testRulesRequestFail() + { + $response = $this->json('GET', sprintf('/day-validation?day=%s', '"!:')); + $response->assertJsonValidationErrors('day'); + } + + public function testRulesRequestInvalid() + { + $this->expectException(UnknownEnumClass::class); + new EnumIsValidRule(SplFileObject::class); + } + + public function testNotRoutableEnum() + { + $response = $this->json('GET', sprintf('/notroutable/%s', '1')); + $response->assertOk(); + } +} diff --git a/tests/BaseEnumTest.php b/tests/BaseEnumTest.php new file mode 100644 index 0000000..df3221b --- /dev/null +++ b/tests/BaseEnumTest.php @@ -0,0 +1,175 @@ +assertEquals('Monday', $mon->label, 'Days::mon()->value should be equals to `Monday`'); + $this->assertEquals(2, $mon->value, 'Days::mon()->value should be equals to `2`'); + $this->assertClassHasAttribute('value', Days::class, 'Days should have a value property'); + $this->assertClassHasAttribute('label', Days::class, 'Days should have a label property'); + + // undef prop test + $exception = null; + try { + $mon->propthatdontexists; + } catch (Exception $e) { + $exception = $e; + } finally { + $this->assertEquals( + UnknownEnumProperty::class, + \is_object($exception) ? \get_class($exception) : $exception, + 'Accessing non existent prop on base enum should throw an UnknownEnumProperty exception' + ); + } + + $this->assertEquals('Days', $mon->getEnumName(), 'getEnumName should return the enum class name'); + $this->assertEquals('mon', $mon->getDefinition(), 'Days::mon() definition should be `mon`'); + $this->assertFalse($mon->equals($tue), 'Days::mon() should not be equals to Days::tue()'); + $this->assertTrue($mon->equals($mon), 'Days::mon() should be equals to Days::mon()'); + $this->assertIsArray(Days::toArray(), 'toArray method should return an array'); + $this->assertIsArray(Days::toValues(), 'toValues method should return an array'); + $this->assertIsArray(Days::toLabels(), 'toLabels method should return an array'); + $this->assertEquals([ + 0 => 1, + 1 => 2, + 2 => 4, + 3 => 8, + 4 => 16, + 5 => 32, + 6 => 64, + 7 => 128 + ], Days::toValues(), 'Days::toValues method should return an integer array'); + $this->assertEquals([ + 0 => 'None', + 1 => 'Monday', + 2 => 'Tuesday', + 3 => 'Wednesday', + 4 => 'Thursday', + 5 => 'Friday', + 6 => 'Saturday', + 7 => 'Sunday' + ], Days::toLabels(), 'Days::toLabels method should return a string array'); + $this->assertEquals([ + 0 => Days::none(), + 1 => Days::mon(), + 2 => Days::tue(), + 3 => Days::wed(), + 4 => Days::thu(), + 5 => Days::fri(), + 6 => Days::sat(), + 7 => Days::sun() + ], Days::toArray(), 'Days::toLabels method should return an enum array'); + $this->assertEquals( + (string)ForceStringsFromInteger::valueA()->value, + (string)ForceStringsFromInteger::valueA(), + 'BaseEnum::__toString should return the value as a string' + ); + } + + public function testCannotAssignAnEnumValue() + { + $this->expectError(); + $enum = Days::mon(); + $enum->value = 'anything'; + } + + public function testCannotAssignAnEnumLabel() + { + $this->expectError(); + $enum = Days::mon(); + $enum->label = 'anything'; + } + + /** + * This method is here for code coverage + * + * @return void + */ + public function testHackingEnumValueCouldConfuseCache() + { + $enum = Days::mon(); + $refClass = new ReflectionClass($enum); + $property = $refClass->getProperty('value'); + $property->setAccessible(true); + $property->setValue($enum, 'anything'); + $this->assertEquals('anything', $enum->value); + $this->assertNull( + $enum->getDefinition(), + 'BaseEnum::getDefinition should be null when hacking var consing cache' + ); + } + + public function testValidEnumSet() + { + $this->expectNotToPerformAssertions(); + $report = new Report(); + $report->day = Days::mon(); + $report->day = Days::mon()->value; + } + + public function testInvalidEnumSet() + { + $this->expectException(BadMethodCallException::class); + $report = new Report(); + $report->day = Days::mon()->label; + } + + public function testInvalidEnumSetWithFloat() + { + $this->expectException(TypeError::class); + $report = new Report(); + $report->day = 0.1; + } + + public function testInvalidEnumSetNotAuthorizedType() + { + $this->expectException(TypeError::class); + $report = new Report(); + $report->day = new stdClass(); + } + + public function testInvalidEnumCanSetNull() + { + $this->expectNotToPerformAssertions(); + $report = new Report(); + $report->day = null; + } + + public function testDuplicatedValues() + { + $this->expectException(DuplicateValuesException::class); + DuplicatedValues::one(); + } + + public function testDuplicatedLabels() + { + $this->expectException(DuplicateLabelsException::class); + DuplicatedLabels::one(); + } +} diff --git a/tests/EnumTest.php b/tests/EnumTest.php deleted file mode 100644 index f3fcf77..0000000 --- a/tests/EnumTest.php +++ /dev/null @@ -1,121 +0,0 @@ -alias(Days::class, 'Days'); - $app->alias(ContactFormCivility::class, 'ContactFormCivility'); - $app->alias(ForceStringsFromInteger::class, 'ForceStringsFromInteger'); - } - - /** - * Define routes setup. - * - * @param \Illuminate\Routing\Router $router - * - * @return void - */ - protected function defineRoutes($router) - { - $router->get('/days/{day}', function (Days $day) { - return response()->json([$day]); - })->middleware('bindings'); - $router->get('/civilities/{civility}', function (ContactFormCivility $civility) { - return response()->json([$civility]); - })->middleware('bindings'); - $router->get('/force/{force}', function (ForceStringsFromInteger $force) { - return response()->json([$force]); - })->middleware('bindings'); - } - - - public function testEnum() - { - $mon = Days::mon(); - $tue = Days::tue(); - Days::toArray(); - Days::toValues(); - Days::toLabels(); - json_encode($tue); - $mon->getDefinition(); - $this->assertClassHasAttribute('value', Days::class); - $this->assertClassHasAttribute('label', Days::class); - $this->assertFalse($mon->equals($tue)); - $this->assertTrue($mon->equals($mon)); - $this->assertIsArray(Days::toArray()); - $this->assertIsArray(Days::toValues()); - $this->assertIsArray(Days::toLabels()); - $this->assertEquals([ - 0 => 0, - 1 => 1, - 2 => 2, - 3 => 4, - 4 => 8, - 5 => 16, - 6 => 32, - 7 => 64 - ], Days::toValues()); - $this->assertEquals([ - 0 => 'None', - 1 => 'Monday', - 2 => 'Tuesday', - 3 => 'Wednesday', - 4 => 'Thursday', - 5 => 'Friday', - 6 => 'Saturday', - 7 => 'Sunday' - ], Days::toLabels()); - $this->assertEquals([ - 0 => Days::none(), - 1 => Days::mon(), - 2 => Days::tue(), - 3 => Days::wed(), - 4 => Days::thu(), - 5 => Days::fri(), - 6 => Days::sat(), - 7 => Days::sun() - ], Days::toArray()); - $this->assertEquals('Monday', (string)$mon); - } - - public function testRoutesDays() - { - $response = $this->call('GET', '/days/1'); - $response->assertOk(); - $response->assertExactJson([Days::mon()]); - $response = $this->call('GET', '/days/2'); - $response->assertOk(); - $response->assertExactJson([Days::tue()]); - } - - public function testRoutesCivilities() - { - $response = $this->call('GET', '/civilities/mme'); - $response->assertOk(); - $response->assertExactJson([ContactFormCivility::mme()]); - } - - public function testRoutesWithEnumIntString() - { - $response = $this->call('GET', '/force/1'); - $response->assertOk(); - $response->assertExactJson([ForceStringsFromInteger::valueB()]); - } - - public function testRules() - { - $rule = new EnumIsValidRule(Days::class); - $this->assertTrue($rule->passes('day', Days::mon()->value)); - $this->assertFalse($rule->passes('day', 9999)); - $this->assertFalse($rule->passes('day', Days::mon())); - } -} diff --git a/tests/Enums/Days.php b/tests/Enums/Days.php index 0de7126..db83d77 100644 --- a/tests/Enums/Days.php +++ b/tests/Enums/Days.php @@ -19,14 +19,14 @@ final class Days extends BaseEnumRoutable protected static function values(): array { return [ - 'none' => 0, - 'mon' => 1, - 'tue' => 1 << 1, - 'wed' => 1 << 2, - 'thu' => 1 << 3, - 'fri' => 1 << 4, - 'sat' => 1 << 5, - 'sun' => 1 << 6, + 'none' => 1, + 'mon' => 1 << 1, + 'tue' => 1 << 2, + 'wed' => 1 << 3, + 'thu' => 1 << 4, + 'fri' => 1 << 5, + 'sat' => 1 << 6, + 'sun' => 1 << 7, ]; } diff --git a/tests/Enums/DuplicatedLabels.php b/tests/Enums/DuplicatedLabels.php new file mode 100644 index 0000000..0001202 --- /dev/null +++ b/tests/Enums/DuplicatedLabels.php @@ -0,0 +1,22 @@ + 'one', + 'two' => 'two', + 'three' => 'two' + ]; + } +} diff --git a/tests/Enums/DuplicatedValues.php b/tests/Enums/DuplicatedValues.php new file mode 100644 index 0000000..b7a7df8 --- /dev/null +++ b/tests/Enums/DuplicatedValues.php @@ -0,0 +1,22 @@ + '0', + 'two' => '1', + 'three' => '1' + ]; + } +} diff --git a/tests/Enums/NotRoutableEnum.php b/tests/Enums/NotRoutableEnum.php new file mode 100644 index 0000000..50ec920 --- /dev/null +++ b/tests/Enums/NotRoutableEnum.php @@ -0,0 +1,22 @@ + '0', + 'two' => '1', + 'three' => '2' + ]; + } +} diff --git a/tests/Models/Alarm.php b/tests/Models/Alarm.php new file mode 100644 index 0000000..c12a0ec --- /dev/null +++ b/tests/Models/Alarm.php @@ -0,0 +1,23 @@ + 'int' + ]; + + protected $enumCasts = [ + 'day' => Days::class + ]; +} diff --git a/tests/Models/Report.php b/tests/Models/Report.php new file mode 100644 index 0000000..763d673 --- /dev/null +++ b/tests/Models/Report.php @@ -0,0 +1,28 @@ + Days::class + ]; +} diff --git a/tests/Requests/PassDayRequest.php b/tests/Requests/PassDayRequest.php new file mode 100644 index 0000000..5e8fcbb --- /dev/null +++ b/tests/Requests/PassDayRequest.php @@ -0,0 +1,32 @@ + ['required', new EnumIsValidRule(Days::class)], + ]; + } +}