From 2be8e565bf503e99735ebe63bfdbbc14d96954dd Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sat, 31 Dec 2022 16:25:16 +0100 Subject: [PATCH] Refactored UUID generation: use pseudo unique named UUID for schedules --- includes/pages/admin_shifts.php | 27 ++-- includes/pages/schedule/ImportSchedule.php | 2 + resources/lang/de_DE/default.po | 3 + resources/lang/en_US/default.po | 3 + src/Helpers/Uuid.php | 62 ++++++++ src/Helpers/UuidServiceProvider.php | 19 +-- .../Unit/Helpers/UuidServiceProviderTest.php | 15 +- tests/Unit/Helpers/UuidTest.php | 132 ++++++++++++++++++ 8 files changed, 229 insertions(+), 34 deletions(-) create mode 100644 src/Helpers/Uuid.php create mode 100644 tests/Unit/Helpers/UuidTest.php diff --git a/includes/pages/admin_shifts.php b/includes/pages/admin_shifts.php index 33fad4e9..ba24b9dc 100644 --- a/includes/pages/admin_shifts.php +++ b/includes/pages/admin_shifts.php @@ -5,6 +5,7 @@ use Engelsystem\Helpers\Carbon; use Engelsystem\Http\Exceptions\HttpForbidden; use Engelsystem\Models\AngelType; use Engelsystem\Models\Room; +use Engelsystem\Models\Shifts\Schedule; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Models\User\User; @@ -208,7 +209,7 @@ function admin_shifts() 'description' => $description, ]; } elseif ($mode == 'multi') { - $shift_start = $start; + $shift_start = $start; do { $shift_end = (clone $shift_start)->addSeconds((int) $length * 60); @@ -589,22 +590,26 @@ function admin_shifts_history(): string throw_redirect(page_link_to('admin_shifts_history')); } + $schedules = Schedule::all()->pluck('name', 'id')->toArray(); $shiftsData = Db::select(' SELECT - transaction_id, - title, - COUNT(id) AS count, - MIN(start) AS start, - MAX(end) AS end, - created_by AS user_id, - MAX(created_at) AS created_at - FROM shifts - WHERE transaction_id IS NOT NULL - GROUP BY transaction_id + s.transaction_id, + s.title, + schedule_shift.schedule_id, + COUNT(s.id) AS count, + MIN(s.start) AS start, + MAX(s.end) AS end, + s.created_by AS user_id, + MAX(s.created_at) AS created_at + FROM shifts AS s + LEFT JOIN schedule_shift on schedule_shift.shift_id = s.id + WHERE s.transaction_id IS NOT NULL + GROUP BY s.transaction_id ORDER BY created_at DESC '); foreach ($shiftsData as &$shiftData) { + $shiftData['title'] = $shiftData['schedule_id'] ? __('shifts_history.schedule', [$schedules[$shiftData['schedule_id']]]) : $shiftData['title']; $shiftData['user'] = User_Nick_render(User::find($shiftData['user_id'])); $shiftData['start'] = Carbon::make($shiftData['start'])->format(__('Y-m-d H:i')); $shiftData['end'] = Carbon::make($shiftData['end'])->format(__('Y-m-d H:i')); diff --git a/includes/pages/schedule/ImportSchedule.php b/includes/pages/schedule/ImportSchedule.php index f71b0075..3538e011 100644 --- a/includes/pages/schedule/ImportSchedule.php +++ b/includes/pages/schedule/ImportSchedule.php @@ -12,6 +12,7 @@ use Engelsystem\Helpers\Schedule\Event; use Engelsystem\Helpers\Schedule\Room; use Engelsystem\Helpers\Schedule\Schedule; use Engelsystem\Helpers\Schedule\XmlParser; +use Engelsystem\Helpers\Uuid; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Models\Room as RoomModel; @@ -304,6 +305,7 @@ class ImportSchedule extends BaseController $shift->end = $event->getEndDate()->copy()->timezone($eventTimeZone); $shift->room()->associate($room); $shift->url = $event->getUrl() ?? ''; + $shift->transaction_id = Uuid::uuidBy($scheduleUrl->id, '5c4ed01e'); $shift->createdBy()->associate($user); $shift->save(); diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index 570a5abf..68864dca 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -2868,6 +2868,9 @@ msgstr "Titel" msgid "schedule.import.shift.room" msgstr "Raum" +msgid "shifts_history.schedule" +msgstr "Programm: %s" + msgid "news.title" msgstr "News" diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index 7dd17f49..7417fdc2 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -145,6 +145,9 @@ msgstr "Title" msgid "schedule.import.shift.room" msgstr "Room" +msgid "shifts_history.schedule" +msgstr "Schedule: %s" + msgid "news.title" msgstr "News" diff --git a/src/Helpers/Uuid.php b/src/Helpers/Uuid.php new file mode 100644 index 00000000..b35ee0b7 --- /dev/null +++ b/src/Helpers/Uuid.php @@ -0,0 +1,62 @@ + 20) { + throw new InvalidArgumentException('$name is longer than 20 characters'); + } + + $name = Str::lower($name); + } + + $value = $name . md5((string) $value); + + return sprintf( + '%08s-%04s-%04s-%04s-%012s', + Str::substr($value, 0, 8), + Str::substr($value, 8, 4), + // first bit is the uuid version, here 4 + '4' . Str::substr($value, 13, 3), + // first bit is the variant (0x8-0xb) + dechex(8 + (hexdec(Str::substr($value, 16, 1)) % 4)) + . Str::substr($value, 17, 3), + Str::substr($value, 20, 12) + ); + } +} diff --git a/src/Helpers/UuidServiceProvider.php b/src/Helpers/UuidServiceProvider.php index 5bdb0708..82d25dd4 100644 --- a/src/Helpers/UuidServiceProvider.php +++ b/src/Helpers/UuidServiceProvider.php @@ -12,23 +12,6 @@ class UuidServiceProvider extends ServiceProvider */ public function register(): void { - Str::createUuidsUsing([$this, 'uuid']); - } - - /** - * Generate a v4 UUID - */ - public function uuid(): string - { - return sprintf( - '%08x-%04x-%04x-%04x-%012x', - mt_rand(0, 0xffffffff), - mt_rand(0, 0xffff), - // first bit is the uuid version, here 4 - mt_rand(0, 0x0fff) | 0x4000, - // variant - mt_rand(0, 0x3fff) | 0x8000, - mt_rand(0, 0xffffffffffff) - ); + Str::createUuidsUsing(Uuid::class . '::uuid'); } } diff --git a/tests/Unit/Helpers/UuidServiceProviderTest.php b/tests/Unit/Helpers/UuidServiceProviderTest.php index 36094510..b867fc7b 100644 --- a/tests/Unit/Helpers/UuidServiceProviderTest.php +++ b/tests/Unit/Helpers/UuidServiceProviderTest.php @@ -3,15 +3,16 @@ namespace Engelsystem\Test\Unit\Helpers; use Engelsystem\Application; +use Engelsystem\Helpers\Uuid; use Engelsystem\Helpers\UuidServiceProvider; use Engelsystem\Test\Unit\ServiceProviderTest; use Illuminate\Support\Str; +use ReflectionProperty; class UuidServiceProviderTest extends ServiceProviderTest { /** * @covers \Engelsystem\Helpers\UuidServiceProvider::register - * @covers \Engelsystem\Helpers\UuidServiceProvider::uuid */ public function testRegister(): void { @@ -20,9 +21,13 @@ class UuidServiceProviderTest extends ServiceProviderTest $serviceProvider = new UuidServiceProvider($app); $serviceProvider->register(); - $this->assertStringMatchesFormat( - '%x%x%x%x%x%x%x%x-%x%x%x%x-4%x%x%x-%x%x%x%x-%x%x%x%x%x%x%x%x%x%x%x%x', - Str::uuid() - ); + $uuidFactoryReference = (new ReflectionProperty(Str::class, 'uuidFactory')) + ->getValue(); + + $this->assertIsCallable($uuidFactoryReference); + $this->assertIsString($uuidFactoryReference); + $this->assertEquals(Uuid::class . '::uuid', $uuidFactoryReference); + + $this->assertTrue(Str::isUuid(Str::uuid()), 'Is a UUID'); } } diff --git a/tests/Unit/Helpers/UuidTest.php b/tests/Unit/Helpers/UuidTest.php new file mode 100644 index 00000000..521c9fdb --- /dev/null +++ b/tests/Unit/Helpers/UuidTest.php @@ -0,0 +1,132 @@ +uuid(); + + $this->checkUuid4Format($result); + } + + public function generateUuidBy(): array + { + return [ + [42, 'a1d0c6e8-3f02-4327-9846-1063f4ac58a6'], + ['42', 'a1d0c6e8-3f02-4327-9846-1063f4ac58a6'], + [1.23, '579c4c7f-58e4-45e8-8cfc-73e08903a08c'], + ['1.23', '579c4c7f-58e4-45e8-8cfc-73e08903a08c'], + ['test', '098f6bcd-4621-4373-8ade-4e832627b4f6'] + ]; + } + + /** + * @covers \Engelsystem\Helpers\Uuid::uuidBy + * @dataProvider generateUuidBy + */ + public function testUuidBy(mixed $value, string $expected): void + { + $uuid = new Uuid(); + $result = $uuid->uuidBy($value); + + $this->checkUuid4Format($result); + $this->assertEquals($expected, $result); + } + + public function generateUuidByNumbers(): array + { + $numbers = []; + foreach (range(0, 10) as $number) { + $numbers[] = [$number]; + } + + return $numbers; + } + + /** + * @covers \Engelsystem\Helpers\Uuid::uuidBy + * @dataProvider generateUuidByNumbers + */ + public function testUuidByNumbers(mixed $value): void + { + $uuid = new Uuid(); + $result = $uuid->uuidBy($value); + + $this->checkUuid4Format($result); + } + + public function generateUuidByNamed(): array + { + return [ + ['42', 42, '42a1d0c6-e83f-4273-a7d8-461063f4ac58'], + ['123', 1.23, '123579c4-c7f5-4e48-9e8c-cfc73e08903a'], + ['7e57', 'test', '7e57098f-6bcd-4621-9373-cade4e832627'], + ['5c4ed01eda7a1490751', 'schedule data', '5c4ed01e-da7a-4490-b514-33ffb998ffe8'], + ['00001000010000100001', '20 characters', '00001000-0100-4010-8001-3e8c8aa31133'], + ['ABC0DEF', 'lowercase', 'abc0deff-8241-4ecc-87fb-74bf40ccfe96'], + ['0123456789000abc0def', 'change nothing', '01234567-8900-4abc-8def-4a28d7089c93'], + ]; + } + + /** + * @covers \Engelsystem\Helpers\Uuid::uuidBy + * @dataProvider generateUuidByNamed + */ + public function testUuidByNamed(string $name, mixed $value, string $expected): void + { + $uuid = new Uuid(); + $result = $uuid->uuidBy($value, $name); + + $this->checkUuid4Format($result); + $this->assertEquals($expected, $result); + } + + /** + * @covers \Engelsystem\Helpers\Uuid::uuidBy + */ + public function testUuidByNamedTooLong(): void + { + $uuid = new Uuid(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/\$name.+20.+/'); + $uuid->uuidBy('', '111111111111111111111'); + } + + /** + * @covers \Engelsystem\Helpers\Uuid::uuidBy + */ + public function testUuidByNamedNotHex(): void + { + $uuid = new Uuid(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/\$name.+hex.+/'); + $uuid->uuidBy('', 'not a hex name'); + } + + protected function checkUuid4Format(string $uuid): void + { + $version = Str::substr($uuid, 14, 1); + $variant = Str::substr($uuid, 19, 1); + + $this->assertTrue(Str::isUuid($uuid), 'Is a UUID'); + $this->assertEquals(4, $version, 'Version'); + $this->assertStringStartsWith( + '10', + sprintf('%04b', hexdec($variant)), + 'Variant is 0x8-0xb (RFC 4122, DCE 1.1, ISO/IEC 11578:1996)' + ); + } +}