551 lines
20 KiB
PHP
551 lines
20 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';
|
|
}
|
|
|
|
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 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::$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 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 function reset(): void
|
|
{
|
|
self::$sent = [];
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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.');
|
|
},
|
|
],
|
|
[
|
|
'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.');
|
|
},
|
|
],
|
|
[
|
|
'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.');
|
|
},
|
|
],
|
|
[
|
|
'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(\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.');
|
|
},
|
|
],
|
|
[
|
|
'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(\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\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);
|
|
}
|