customers = new \stdClass(); $this->customers->customers = new class { public function search($params, $options): object { \classes\economic::$search_calls[] = [ 'params' => $params, 'options' => $options, ]; $mock = new \stdClass(); $mock->collection = \classes\economic::$mock_collection; return $mock; } }; } public function createCustomer($number, $name, $cvr, $email, $phone, $mobilePhone = null, $companyInformation = null): object { self::$create_calls[] = [ 'number' => (int)$number, 'name' => (string)$name, 'cvr' => (string)$cvr, 'email' => (string)$email, 'phone' => (int)$phone, 'mobile_phone' => $mobilePhone === null ? null : (int)$mobilePhone, 'company_information' => $companyInformation, ]; $response = self::$mock_create_response ?? (object)[ 'customerNumber' => (int)$number, ]; if (isset($response->customerNumber) && is_numeric($response->customerNumber)) { \objects\users_o::$mock_importable_customer_numbers[] = (int)$response->customerNumber; \objects\users_o::$mock_importable_customer_numbers = array_values(array_unique(\objects\users_o::$mock_importable_customer_numbers)); } return $response; } } class virkdata { public static string $mock_name = 'Mock Company'; public static string $mock_address = 'Demo Street 1'; public static int $mock_zipcode = 2630; public static string $mock_city = 'Taastrup'; public static string $mock_website = 'https://demo.test'; public function getCompanyInformation($cvr, $endpoint, $data): object { $result = new \stdClass(); $result->name = self::$mock_name; $result->address = self::$mock_address; $result->zipcode = self::$mock_zipcode; $result->city = self::$mock_city; $result->website = self::$mock_website; return $result; } } class email { public static array $sent = []; public static array $superuser_notifications = []; public static function reset(): void { self::$sent = []; self::$superuser_notifications = []; } public function sendWelcomeEmailToCustomer($phone, $email): bool { self::$sent[] = [ 'customer_number' => (int)$phone, 'email' => (string)$email, ]; \objects\users_o::$interaction_log[] = 'email:' . (int)$phone . ':' . (string)$email; return true; } public function sendNewCustomerRegistrationNotifications($phone): bool { self::$superuser_notifications[] = [ 'customer_number' => (int)$phone, ]; \objects\users_o::$interaction_log[] = 'superuser-notification:' . (int)$phone; return true; } } class slack { public static array $customer_registration_notifications = []; public static function reset(): void { self::$customer_registration_notifications = []; } public function send_customer_registration_notification($customer_number): self { self::$customer_registration_notifications[] = [ 'customer_number' => (int)$customer_number, ]; \objects\users_o::$interaction_log[] = 'slack-customer-registration:' . (int)$customer_number; return $this; } } class authentication { public function get_plate_scanner(): bool { return true; } } } namespace objects { class users_o { public static array $mock_existing_customer_numbers = []; public static array $mock_importable_customer_numbers = []; public static array $interaction_log = []; public int $id = 0; private bool $exists = false; public static function reset(): void { self::$mock_existing_customer_numbers = []; self::$mock_importable_customer_numbers = []; self::$interaction_log = []; } public function getFieldsWhere(array $fieldsAndValues, array $fields): array { $customerNumber = (int)($fieldsAndValues['customer_number'] ?? 0); if (in_array($customerNumber, self::$mock_existing_customer_numbers, true)) { return [['id' => $customerNumber]]; } return []; } public function getUserByCustomerNumber($num): self { $customerNumber = (int)$num; self::$interaction_log[] = 'bootstrap:' . $customerNumber; $existsLocally = in_array($customerNumber, self::$mock_existing_customer_numbers, true); $canImport = in_array($customerNumber, self::$mock_importable_customer_numbers, true); if ($existsLocally || $canImport) { $this->id = $customerNumber; $this->exists = true; if (!$existsLocally) { self::$mock_existing_customer_numbers[] = $customerNumber; self::$mock_existing_customer_numbers = array_values(array_unique(self::$mock_existing_customer_numbers)); } } return $this; } public function exists(): bool { return $this->exists; } } class logs_o { public static array $entries = []; public static function reset(): void { self::$entries = []; } public function add($module, $department, $type, $user_id, $event, $details): void { self::$entries[] = [ 'module' => (string)$module, 'department' => (string)$department, 'type' => (int)$type, 'user_id' => (int)$user_id, 'event' => (string)$event, 'details' => (string)$details, ]; } } class tokens_o { public function delete($token): void { } } class customer_password_reset_keys_o { public static function generateToken(): string { return 'mock_token'; } public function add($data): void { } public function findValidByToken($token) { return null; } } } namespace { class MockRouter { public array $routes = []; public function add($route, $method, $callback, $permissions): void { $this->routes[$method][$route] = $callback; } } function ok(string $message): void { echo "\033[32m[PASS]\033[0m $message\n"; } function fail(string $message): void { echo "\033[31m[FAIL]\033[0m $message\n"; } function assert_true(bool $condition, string $message): void { if (!$condition) { throw new \RuntimeException($message); } } function assert_same_data(mixed $actual, mixed $expected, string $message): void { $normalize = static fn(mixed $value): string => (string)json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($normalize($actual) !== $normalize($expected)) { throw new \RuntimeException($message . ' Expected ' . $normalize($expected) . ' but got ' . $normalize($actual)); } } $router = new MockRouter(); $response = new \classes\response(); require_once WD . '/traits/route_t.php'; require_once WD . '/routes/authRoute.php'; $authRoute = new \routes\authRoute(); $authRoute->run(); if (!isset($router->routes['POST']['/auth/register/cvr'])) { die("Route /auth/register/cvr not found\n"); } $callback = $router->routes['POST']['/auth/register/cvr']; $baseParams = [ 'cvr' => '12345678', 'companyPhone' => 12345678, 'invoiceEmail' => 'test@test.com', 'contactEmail' => 'test@test.com', 'contactPhone' => 12345678, 'g_recaptcha_response' => 'valid', ]; $testCases = [ [ 'name' => 'Missing reCAPTCHA', 'params' => [], 'setup' => static function (): void { \classes\recaptcha::$mock_valid = false; }, 'expected_error' => 'Authentication failed. Invalid or missing reCAPTCHA.', 'expected_status' => 401, ], [ 'name' => 'Missing parameters', 'params' => ['g_recaptcha_response' => 'valid'], 'expected_error' => 'Missing required parameters: cvr, companyPhone, invoiceEmail, contactEmail, contactPhone', 'expected_status' => 400, ], [ 'name' => 'Invalid CVR length (too short)', 'params' => array_merge($baseParams, ['cvr' => '123']), 'expected_error' => 'Parameter cvr must be at least 8 characters long', 'expected_status' => 400, ], [ 'name' => 'Existing company phone with local customer stays blocked', 'params' => $baseParams, 'setup' => static function (): void { \objects\users_o::$mock_existing_customer_numbers = [12345678]; }, 'expected_error' => 'Company phone number already registered', 'expected_status' => 400, 'assert' => static function (): void { assert_true(count(\classes\economic::$create_calls) === 0, 'Fresh create must not run for local duplicates.'); assert_true(count(\classes\email::$sent) === 0, 'Duplicate registrations must not send welcome emails.'); assert_true(count(\classes\email::$superuser_notifications) === 0, 'Duplicate registrations must not send superuser notifications.'); assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'Duplicate registrations must not send Slack customer registration notifications.'); }, ], [ 'name' => 'Existing matching e-conomic customer recovers partial local registration', 'params' => $baseParams, 'setup' => static function (): void { \classes\economic::$mock_collection = [ (object)[ 'customerNumber' => 12345678, 'name' => 'Recovered Company', ], ]; \objects\users_o::$mock_importable_customer_numbers = [12345678]; }, 'expected_success' => (object)[ 'customerNumber' => 12345678, 'name' => 'Recovered Company', ], 'expected_status' => 200, 'assert' => static function (): void { assert_true(count(\classes\economic::$create_calls) === 0, 'Recovery must not create a second e-conomic customer.'); assert_true(count(\classes\email::$sent) === 2, 'Recovery must send two welcome emails.'); assert_true(\objects\users_o::$interaction_log[0] === 'bootstrap:12345678', 'Local bootstrap must happen before sending emails.'); assert_true(\classes\email::$sent[0]['email'] === 'jm@truckwash.dk', 'Recovery should notify the internal welcome recipient first.'); assert_true(\classes\email::$sent[1]['email'] === 'test@test.com', 'Recovery should notify the invoice email.'); assert_true(count(\classes\email::$superuser_notifications) === 1, 'Recovery must notify opted-in superusers once.'); assert_true(\classes\email::$superuser_notifications[0]['customer_number'] === 12345678, 'Recovery superuser notification must use the recovered customer number.'); assert_true(count(\classes\slack::$customer_registration_notifications) === 1, 'Recovery must notify Slack once.'); assert_true(\classes\slack::$customer_registration_notifications[0]['customer_number'] === 12345678, 'Recovery Slack notification must use the recovered customer number.'); }, ], [ 'name' => 'Existing matching e-conomic customer with local user still returns duplicate', 'params' => $baseParams, 'setup' => static function (): void { \classes\economic::$mock_collection = [ (object)[ 'customerNumber' => 12345678, 'name' => 'Recovered Company', ], ]; \objects\users_o::$mock_existing_customer_numbers = [12345678]; }, 'expected_error' => 'Company phone number already registered', 'expected_status' => 400, 'assert' => static function (): void { assert_true(count(\classes\email::$sent) === 0, 'Duplicate recovery attempts must not send welcome emails.'); assert_true(count(\classes\email::$superuser_notifications) === 0, 'Duplicate recovery attempts must not send superuser notifications.'); assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'Duplicate recovery attempts must not send Slack customer registration notifications.'); }, ], [ 'name' => 'Existing mismatched e-conomic customer returns conflict', 'params' => $baseParams, 'setup' => static function (): void { \classes\economic::$mock_collection = [ (object)[ 'customerNumber' => 87654321, 'name' => 'Wrong Number Company', ], ]; }, 'expected_error' => 'CVR already registered under customer number 87654321. The submitted phone number must match the customer id. Manual cleanup or reassignment is required before retrying.', 'expected_status' => 409, 'assert' => static function (): void { assert_true(count(\classes\economic::$create_calls) === 0, 'Conflict on existing mismatched customer must not trigger create.'); assert_true(count(\classes\email::$sent) === 0, 'Conflict on existing mismatched customer must not send welcome emails.'); assert_true(count(\classes\email::$superuser_notifications) === 0, 'Conflict on existing mismatched customer must not send superuser notifications.'); assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'Conflict on existing mismatched customer must not send Slack customer registration notifications.'); assert_true(count(\objects\logs_o::$entries) === 1, 'Conflict should be logged for manual cleanup.'); }, ], [ 'name' => 'Successful registration bootstraps local user before welcome emails', 'params' => array_merge($baseParams, ['contactPhone' => 87654320]), 'setup' => static function (): void { \classes\economic::$mock_create_response = (object)[ 'customerNumber' => 12345678, 'name' => 'Mock Company', ]; }, 'expected_success' => (object)[ 'customerNumber' => 12345678, 'name' => 'Mock Company', ], 'expected_status' => 201, 'assert' => static function (): void { assert_true(count(\classes\economic::$create_calls) === 1, 'Fresh registration must call create exactly once.'); assert_true(\classes\economic::$create_calls[0]['phone'] === 12345678, 'Fresh registration must use the company phone as the e-conomic customer phone.'); assert_true(\classes\economic::$create_calls[0]['mobile_phone'] === 87654320, 'Fresh registration must pass the contact phone as the e-conomic mobile phone.'); $companyInformation = \classes\economic::$create_calls[0]['company_information']; assert_true(is_object($companyInformation), 'Fresh registration must pass CVR company information to e-conomic.'); assert_true($companyInformation->address === 'Demo Street 1', 'Fresh registration must pass the CVR address to e-conomic.'); assert_true($companyInformation->zipcode === 2630, 'Fresh registration must pass the CVR zipcode to e-conomic.'); assert_true($companyInformation->city === 'Taastrup', 'Fresh registration must pass the CVR city to e-conomic.'); assert_true($companyInformation->website === 'https://demo.test', 'Fresh registration must pass the CVR website to e-conomic.'); assert_true(count(\classes\email::$sent) === 2, 'Fresh registration must send two welcome emails.'); assert_true(\objects\users_o::$interaction_log[0] === 'bootstrap:12345678', 'Fresh registration must bootstrap the local user before emails.'); assert_true(\classes\email::$sent[0]['email'] === 'jm@truckwash.dk', 'Fresh registration should notify the internal welcome recipient first.'); assert_true(count(\classes\email::$superuser_notifications) === 1, 'Fresh registration must notify opted-in superusers once.'); assert_true(\classes\email::$superuser_notifications[0]['customer_number'] === 12345678, 'Fresh registration superuser notification must use the created customer number.'); assert_true(count(\classes\slack::$customer_registration_notifications) === 1, 'Fresh registration must notify Slack once.'); assert_true(\classes\slack::$customer_registration_notifications[0]['customer_number'] === 12345678, 'Fresh registration Slack notification must use the created customer number.'); }, ], [ 'name' => 'Fresh create mismatch returns conflict without local bootstrap or email', 'params' => $baseParams, 'setup' => static function (): void { \classes\economic::$mock_create_response = (object)[ 'customerNumber' => 87654321, 'logId' => 'abc123', 'message' => 'Customer created with different number', ]; }, 'expected_error' => 'E-conomic created the customer under customer number 87654321 instead of the submitted phone number 12345678. Manual cleanup or reassignment is required before retrying.', 'expected_status' => 409, 'assert' => static function (): void { assert_true(count(\classes\economic::$create_calls) === 1, 'Create mismatch must still record the attempted create call.'); assert_true(count(\objects\users_o::$interaction_log) === 0, 'Create mismatch must not bootstrap the wrong local customer.'); assert_true(count(\classes\email::$sent) === 0, 'Create mismatch must not send welcome emails.'); assert_true(count(\classes\email::$superuser_notifications) === 0, 'Create mismatch must not send superuser notifications.'); assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'Create mismatch must not send Slack customer registration notifications.'); assert_true(count(\objects\logs_o::$entries) === 1, 'Create mismatch should be logged for manual cleanup.'); }, ], ]; $allPassed = true; foreach ($testCases as $test) { \classes\response::reset(); \classes\recaptcha::$mock_valid = true; \classes\economic::reset(); \classes\email::reset(); \classes\slack::reset(); \classes\virkdata::$mock_name = 'Mock Company'; \classes\virkdata::$mock_address = 'Demo Street 1'; \classes\virkdata::$mock_zipcode = 2630; \classes\virkdata::$mock_city = 'Taastrup'; \classes\virkdata::$mock_website = 'https://demo.test'; \objects\users_o::reset(); \objects\logs_o::reset(); if (isset($test['setup'])) { $test['setup'](); } \classes\response::$request_parameters = $test['params']; $issues = []; try { $callback(); $issues[] = 'Callback did not exit as expected.'; } catch (\classes\MockExitException $e) { if (array_key_exists('expected_error', $test)) { if (\classes\response::$last_error !== $test['expected_error']) { $issues[] = 'Expected error "' . $test['expected_error'] . '" but got "' . (\classes\response::$last_error ?? 'NULL') . '".'; } } elseif (array_key_exists('expected_success', $test)) { try { assert_same_data(\classes\response::$last_success, $test['expected_success'], 'Unexpected success payload.'); } catch (\Throwable $assertionFailure) { $issues[] = $assertionFailure->getMessage(); } } if (\classes\response::$last_status !== $test['expected_status']) { $issues[] = 'Expected status ' . $test['expected_status'] . ' but got ' . (\classes\response::$last_status ?? 'NULL') . '.'; } } catch (\Throwable $e) { $issues[] = 'Unexpected exception: ' . $e->getMessage(); } if (empty($issues) && isset($test['assert'])) { try { $test['assert'](); } catch (\Throwable $e) { $issues[] = $e->getMessage(); } } if (empty($issues)) { ok($test['name']); continue; } fail($test['name'] . ' - ' . implode(' ', $issues)); $allPassed = false; } echo "\nRegisterCvrTest completed.\n"; exit($allPassed ? 0 : 1); }