Files
api/services/nginx/app/tests/auth/RegisterCvrTest.php
T
Jeppe B 1e0e051775 Harden Sæby demo registration and department scope (#335)
Complete and secure public customer/driver registration, authoritative limited-backoffice department scope, one-time employee QR login, and pricing concurrency for the Sæby demo.
2026-08-02 11:50:56 +02:00

895 lines
41 KiB
PHP

<?php
namespace {
if (!defined('WD')) {
define('WD', dirname(__DIR__, 2));
}
global $response, $router, $DEBUG;
$DEBUG = true;
$_SERVER['REQUEST_URI'] = '/auth/register/cvr';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
}
namespace classes {
class MockExitException extends \Error {}
class response
{
public static mixed $last_success = null;
public static mixed $last_error = null;
public static ?int $last_status = null;
public static array $request_parameters = [];
public static function reset(): void
{
self::$last_success = null;
self::$last_error = null;
self::$last_status = null;
self::$request_parameters = [];
}
public function success($data, $status = 200): void
{
self::$last_success = $data;
self::$last_status = $status;
throw new MockExitException('SUCCESS_EXIT');
}
public function error($data, $status = 400): void
{
self::$last_error = $data;
self::$last_status = $status;
throw new MockExitException('ERROR_EXIT');
}
public function getRequestParameter($key)
{
return self::$request_parameters[$key] ?? null;
}
public function isRequestParameterSet($key): bool
{
return array_key_exists($key, self::$request_parameters);
}
}
}
namespace classes {
class recaptcha
{
public static bool $mock_valid = true;
public function validate($response): bool
{
return self::$mock_valid;
}
}
class economic
{
public static array $mock_collection = [];
public static ?object $mock_create_response = null;
public static ?\RuntimeException $mock_create_exception = null;
public static array $mock_collection_after_create_exception = [];
public static array $search_calls = [];
public static array $create_calls = [];
public object $customers;
public static function reset(): void
{
self::$mock_collection = [];
self::$mock_create_response = null;
self::$mock_create_exception = null;
self::$mock_collection_after_create_exception = [];
self::$search_calls = [];
self::$create_calls = [];
}
public function __construct()
{
$this->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 static function normalizeCustomerEan(mixed $value): ?string
{
if ($value === null) {
return null;
}
$digits = preg_replace('/\D+/', '', (string)$value);
if (!is_string($digits)) {
return null;
}
$digits = trim($digits);
if ($digits === '') {
return null;
}
if (strlen($digits) > 13) {
throw new \InvalidArgumentException('EAN must be at most 13 digits.');
}
return $digits;
}
public function createCustomer($number, $name, $cvr, $email, $phone, $mobilePhone = null, $companyInformation = null, $ean = 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,
'ean' => $ean === null ? null : (string)$ean,
];
if (self::$mock_create_exception !== null) {
self::$mock_collection = self::$mock_collection_after_create_exception;
throw self::$mock_create_exception;
}
$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 static ?\RuntimeException $mock_exception = null;
public function getCompanyInformation($cvr, $endpoint, $data): object
{
if (self::$mock_exception !== null) {
throw self::$mock_exception;
}
$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 array $failing_recipients = [];
public static function reset(): void
{
self::$sent = [];
self::$superuser_notifications = [];
self::$failing_recipients = [];
}
public function sendWelcomeEmailToCustomer($phone, $email): bool
{
if (in_array((string)$email, self::$failing_recipients, true)) {
throw new \RuntimeException('Mock welcome delivery failure');
}
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 bool $mock_external_lookup_enabled = true;
public static array $interaction_log = [];
public static array $hydrated_contacts = [];
public int $id = 0;
private bool $exists = false;
public static function reset(): void
{
self::$mock_existing_customer_numbers = [];
self::$mock_importable_customer_numbers = [];
self::$mock_external_lookup_enabled = true;
self::$interaction_log = [];
self::$hydrated_contacts = [];
}
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 = self::$mock_external_lookup_enabled
&& 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 importCustomerFromEconomicCustomerData(object $customerData): self|bool
{
$customerNumber = (int)($customerData->customerNumber ?? 0);
if ($customerNumber <= 0) {
return false;
}
self::$interaction_log[] = 'snapshot-import:' . $customerNumber;
$this->id = $customerNumber;
$this->exists = true;
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;
}
public function hydrateRegistrationContact(
string $email,
int $phoneNumber,
int $phoneCountryCode = 45,
?string $contactName = null
): void {
self::$hydrated_contacts[] = [
'customer_number' => $this->id,
'email' => $email,
'phone' => $phoneNumber,
'phone_country_code' => $phoneCountryCode,
'contact_name' => $contactName,
];
self::$interaction_log[] = 'hydrate:' . $this->id;
}
}
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));
}
}
/**
* Each table-driven case is an independent registration request. Clear
* only the two production throttle keys that request can touch so the
* legacy harness does not leak rate-limit state between cases.
*
* @param array<string, mixed> $params
*/
function reset_registration_throttles(array $params): void
{
if (!defined('redis')) {
return;
}
$remoteAddress = trim((string)($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
$keys = [
'auth_public_registration_throttle:customer_ip:' . hash('sha256', $remoteAddress . ':all'),
];
if (isset($params['cvr'], $params['companyPhone'])) {
$identifier = 'cvr:' . (string)$params['cvr'] . ':phone:' . (int)$params['companyPhone'];
$keys[] = 'auth_public_registration_throttle:customer_identity:' . hash('sha256', $identifier);
}
foreach ($keys as $key) {
constant('redis')->delete($key);
}
}
$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,
'contactPhoneCountryCode' => 45,
'contactName' => 'Test Contact',
'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' => 'Invalid EAN length (too long)',
'params' => array_merge($baseParams, ['ean' => '57900012345678']),
'expected_error' => 'EAN must be at most 13 digits.',
'expected_status' => 400,
'assert' => static function (): void {
assert_true(count(\classes\economic::$create_calls) === 0, 'Invalid EAN must not create e-conomic customers.');
assert_true(count(\classes\email::$sent) === 0, 'Invalid EAN must not send welcome emails.');
},
],
[
'name' => 'CVR lookup failure returns validation error without creating customer',
'params' => array_merge($baseParams, ['cvr' => '11111112']),
'setup' => static function (): void {
\classes\virkdata::$mock_exception = new \RuntimeException('An error occurred');
},
'expected_error' => 'CVR could not be verified. Please check the CVR number and try again.',
'expected_status' => 400,
'assert' => static function (): void {
assert_true(count(\classes\economic::$create_calls) === 0, 'CVR lookup failures must not create e-conomic customers.');
assert_true(count(\classes\email::$sent) === 0, 'CVR lookup failures must not send welcome emails.');
assert_true(count(\classes\email::$superuser_notifications) === 0, 'CVR lookup failures must not send superuser notifications.');
assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'CVR lookup failures must not send Slack customer registration notifications.');
assert_true(count(\objects\logs_o::$entries) === 1, 'CVR lookup failures should be logged for diagnostics.');
assert_true(\objects\logs_o::$entries[0]['event'] === 'AUTH_REGISTER_CVR_LOOKUP_FAILED', 'CVR lookup failure should use the lookup failure log event.');
},
],
[
'name' => 'CVR lookup without company name returns validation error without creating customer',
'params' => array_merge($baseParams, ['cvr' => '11111112']),
'setup' => static function (): void {
\classes\virkdata::$mock_name = '';
},
'expected_error' => 'CVR could not be verified. Please check the CVR number and try again.',
'expected_status' => 400,
'assert' => static function (): void {
assert_true(count(\classes\economic::$create_calls) === 0, 'CVR lookup responses without a company name must not create e-conomic customers.');
assert_true(count(\classes\email::$sent) === 0, 'CVR lookup responses without a company name must not send welcome emails.');
assert_true(count(\classes\email::$superuser_notifications) === 0, 'CVR lookup responses without a company name must not send superuser notifications.');
assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'CVR lookup responses without a company name must not send Slack customer registration notifications.');
assert_true(count(\objects\logs_o::$entries) === 1, 'CVR lookup responses without a company name should be logged for diagnostics.');
assert_true(\objects\logs_o::$entries[0]['event'] === 'AUTH_REGISTER_CVR_LOOKUP_INVALID_RESPONSE', 'CVR lookup response without a company name should use the invalid response log event.');
},
],
[
'name' => 'CVR lookup failure takes precedence over local customer number collision',
'params' => array_merge($baseParams, ['cvr' => '11111112']),
'setup' => static function (): void {
\classes\virkdata::$mock_exception = new \RuntimeException('An error occurred');
\objects\users_o::$mock_existing_customer_numbers = [12345678];
},
'expected_error' => 'CVR could not be verified. Please check the CVR number and try again.',
'expected_status' => 400,
'assert' => static function (): void {
assert_true(count(\classes\economic::$create_calls) === 0, 'CVR lookup failures with local collisions must not create e-conomic customers.');
assert_true(count(\classes\email::$sent) === 0, 'CVR lookup failures with local collisions must not send welcome emails.');
assert_true(count(\classes\email::$superuser_notifications) === 0, 'CVR lookup failures with local collisions must not send superuser notifications.');
assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'CVR lookup failures with local collisions must not send Slack customer registration notifications.');
assert_true(count(\objects\logs_o::$entries) === 1, 'CVR lookup failures with local collisions should be logged once.');
assert_true(\objects\logs_o::$entries[0]['event'] === 'AUTH_REGISTER_CVR_LOOKUP_FAILED', 'CVR lookup failure should not be masked by the local duplicate check.');
},
],
[
'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_same_data(\objects\users_o::$hydrated_contacts, [[
'customer_number' => 12345678,
'email' => 'test@test.com',
'phone' => 12345678,
'phone_country_code' => 45,
'contact_name' => 'Test Contact',
]], 'Recovery must hydrate the local registration contact.');
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, 'ean' => '57 90-001234567']),
'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.');
assert_true(\classes\economic::$create_calls[0]['ean'] === '5790001234567', 'Fresh registration must pass normalized EAN to e-conomic.');
assert_same_data(\objects\users_o::$hydrated_contacts, [[
'customer_number' => 12345678,
'email' => 'test@test.com',
'phone' => 87654320,
'phone_country_code' => 45,
'contact_name' => 'Test Contact',
]], 'Fresh registration must hydrate the local registration contact.');
$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' => 'Welcome email failure does not roll back a completed registration',
'params' => $baseParams,
'setup' => static function (): void {
\classes\economic::$mock_create_response = (object)[
'customerNumber' => 12345678,
'name' => 'Mock Company',
];
\classes\email::$failing_recipients = ['test@test.com'];
},
'expected_success' => (object)[
'customerNumber' => 12345678,
'name' => 'Mock Company',
],
'expected_status' => 201,
'assert' => static function (): void {
assert_true(count(\classes\email::$sent) === 1, 'Successful welcome deliveries should be preserved.');
assert_true(count(\classes\email::$superuser_notifications) === 1, 'Superuser notification should continue after welcome email failure.');
assert_true(count(\classes\slack::$customer_registration_notifications) === 1, 'Slack notification should continue after welcome email failure.');
$events = array_column(\objects\logs_o::$entries, 'event');
assert_true(in_array('AUTH_REGISTER_CVR_WELCOME_EMAIL_FAILED', $events, true), 'Welcome email failure should be logged.');
},
],
[
'name' => 'Successful registration falls back to the create response when immediate import lookup misses',
'params' => array_merge($baseParams, ['contactPhone' => 87654320]),
'setup' => static function (): void {
\classes\economic::$mock_create_response = (object)[
'customerNumber' => 12345678,
'name' => 'Mock Company',
'email' => 'test@test.com',
];
\objects\users_o::$mock_external_lookup_enabled = false;
},
'expected_success' => (object)[
'customerNumber' => 12345678,
'name' => 'Mock Company',
'email' => 'test@test.com',
],
'expected_status' => 201,
'assert' => static function (): void {
assert_true(count(\classes\economic::$create_calls) === 1, 'Fresh registration must call create exactly once.');
assert_true(\objects\users_o::$interaction_log[0] === 'bootstrap:12345678', 'Fresh registration must try the standard local bootstrap first.');
assert_true(\objects\users_o::$interaction_log[1] === 'snapshot-import:12345678', 'Fresh registration must import from the create response when the immediate lookup misses.');
assert_true(count(\classes\email::$sent) === 2, 'Snapshot fallback registration must send two welcome emails.');
assert_true(count(\classes\email::$superuser_notifications) === 1, 'Snapshot fallback registration must notify opted-in superusers once.');
assert_true(count(\classes\slack::$customer_registration_notifications) === 1, 'Snapshot fallback registration must notify Slack once.');
},
],
[
'name' => 'Duplicate create response recovers a just-created e-conomic customer and sends notifications',
'params' => $baseParams,
'setup' => static function (): void {
\classes\economic::$mock_create_exception = new \RuntimeException('e-conomic request failed with HTTP 400: Customer already exists');
\classes\economic::$mock_collection_after_create_exception = [
(object)[
'customerNumber' => 12345678,
'name' => 'Recovered After Create',
'email' => 'test@test.com',
],
];
\objects\users_o::$mock_importable_customer_numbers = [12345678];
},
'expected_success' => (object)[
'customerNumber' => 12345678,
'name' => 'Recovered After Create',
'email' => 'test@test.com',
],
'expected_status' => 200,
'assert' => static function (): void {
assert_true(count(\classes\economic::$create_calls) === 1, 'Recovery must still record the attempted create call.');
assert_true(count(\classes\economic::$search_calls) === 2, 'Recovery must verify the duplicate by searching e-conomic again.');
assert_true(count(\classes\email::$sent) === 2, 'Duplicate create recovery must send two welcome emails.');
assert_true(count(\classes\email::$superuser_notifications) === 1, 'Duplicate create recovery must notify opted-in superusers once.');
assert_true(count(\classes\slack::$customer_registration_notifications) === 1, 'Duplicate create recovery must notify Slack once.');
},
],
[
'name' => 'Generic create failure recovers a confirmed just-created e-conomic customer',
'params' => $baseParams,
'setup' => static function (): void {
\classes\economic::$mock_create_exception = new \RuntimeException('e-conomic request failed with HTTP 400: Validation failed. | details={"httpStatusCode":400}');
\classes\economic::$mock_collection_after_create_exception = [
(object)[
'customerNumber' => 12345678,
'name' => 'Recovered After Generic Create Failure',
'email' => 'test@test.com',
],
];
\objects\users_o::$mock_importable_customer_numbers = [12345678];
},
'expected_success' => (object)[
'customerNumber' => 12345678,
'name' => 'Recovered After Generic Create Failure',
'email' => 'test@test.com',
],
'expected_status' => 200,
'assert' => static function (): void {
assert_true(count(\classes\economic::$create_calls) === 1, 'Generic create recovery must still record the attempted create call.');
assert_true(count(\classes\economic::$search_calls) === 2, 'Generic create recovery must confirm the customer by searching e-conomic again.');
assert_true(count(\classes\email::$sent) === 2, 'Generic create recovery must send two welcome emails.');
assert_true(count(\classes\email::$superuser_notifications) === 1, 'Generic create recovery must notify opted-in superusers once.');
assert_true(count(\classes\slack::$customer_registration_notifications) === 1, 'Generic create recovery must notify Slack once.');
},
],
[
'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';
\classes\virkdata::$mock_exception = null;
\objects\users_o::reset();
\objects\logs_o::reset();
reset_registration_throttles($test['params']);
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);
}