149 lines
5.4 KiB
PHP
149 lines
5.4 KiB
PHP
<?php
|
|
|
|
use classes\subuser_contact_verification_service;
|
|
use objects\subusers_o;
|
|
|
|
function subuser_contact_verification_test_subuser(array $values = []): subusers_o
|
|
{
|
|
$subuser = new subusers_o();
|
|
$subuser->id = -1;
|
|
$subuser->getObjectProperties();
|
|
|
|
$defaults = [
|
|
'name' => 'Test Driver',
|
|
'email' => 'driver@example.com',
|
|
'email_verified_at' => null,
|
|
'phone_country_code' => 45,
|
|
'phone' => 12345678,
|
|
'phone_verified_at' => null,
|
|
];
|
|
|
|
foreach ([...$defaults, ...$values] as $property => $value) {
|
|
$subuser->{$property}->set($value);
|
|
}
|
|
|
|
return $subuser;
|
|
}
|
|
|
|
function subuser_contact_verification_test_redis(): object
|
|
{
|
|
return new class {
|
|
public array $values = [];
|
|
public array $ttls = [];
|
|
|
|
public function get(string $key): ?string
|
|
{
|
|
return $this->values[$key] ?? null;
|
|
}
|
|
|
|
public function setEx(string $key, string $value, int $expiration): self
|
|
{
|
|
$this->values[$key] = $value;
|
|
$this->ttls[$key] = $expiration;
|
|
return $this;
|
|
}
|
|
|
|
public function delete(string $key): self
|
|
{
|
|
unset($this->values[$key], $this->ttls[$key]);
|
|
return $this;
|
|
}
|
|
};
|
|
}
|
|
|
|
it('sends and verifies a subuser email verification code without storing plaintext codes', function (): void {
|
|
$redis = subuser_contact_verification_test_redis();
|
|
$sentEmails = [];
|
|
$now = 1_700_000_000;
|
|
$subuser = subuser_contact_verification_test_subuser();
|
|
$service = new subuser_contact_verification_service(
|
|
redis: $redis,
|
|
codeGenerator: fn () => '123456',
|
|
timeProvider: fn () => $now,
|
|
emailSender: function (string $destination, string $code) use (&$sentEmails): void {
|
|
$sentEmails[] = compact('destination', 'code');
|
|
}
|
|
);
|
|
|
|
$delivery = $service->sendCode($subuser, 'email', ['type' => 'test', 'id' => 42]);
|
|
|
|
expect($delivery['status'])->toBe('sent')
|
|
->and($delivery['expires_in'])->toBe(subuser_contact_verification_service::CODE_TTL_SECONDS)
|
|
->and($sentEmails)->toBe([
|
|
[
|
|
'destination' => 'driver@example.com',
|
|
'code' => '123456',
|
|
],
|
|
])
|
|
->and($redis->values)->toHaveCount(1);
|
|
|
|
$storedChallenge = json_decode((string)array_values($redis->values)[0], true);
|
|
expect($storedChallenge['hash'])->not->toBe('123456')
|
|
->and($storedChallenge['attempts'])->toBe(0);
|
|
|
|
$wrong = $service->verifyCode($subuser, 'email', '000000', ['type' => 'test', 'id' => 42]);
|
|
expect($wrong['status'])->toBe('invalid_code');
|
|
|
|
$updatedChallenge = json_decode((string)array_values($redis->values)[0], true);
|
|
expect($updatedChallenge['attempts'])->toBe(1);
|
|
|
|
$verified = $service->verifyCode($subuser, 'email', '123456', ['type' => 'test', 'id' => 42]);
|
|
expect($verified['status'])->toBe('verified')
|
|
->and($subuser->email_verified_at->value())->toBe(date('Y-m-d H:i:s', $now))
|
|
->and($redis->values)->toBe([]);
|
|
|
|
$status = $service->status($subuser);
|
|
expect($status['email']['verified'])->toBeTrue()
|
|
->and($status['phone']['verified'])->toBeFalse()
|
|
->and($status['state'])->toBe('partial');
|
|
});
|
|
|
|
it('clears verification timestamps when subuser contact values change', function (): void {
|
|
$subuser = subuser_contact_verification_test_subuser([
|
|
'email_verified_at' => '2026-07-13 10:00:00',
|
|
'phone_verified_at' => '2026-07-13 10:00:00',
|
|
]);
|
|
$service = new subuser_contact_verification_service(redis: subuser_contact_verification_test_redis());
|
|
|
|
$emailUpdate = $service->clearVerificationForChangedContacts($subuser, [
|
|
'email' => 'new-driver@example.com',
|
|
'phone' => 12345678,
|
|
]);
|
|
expect($emailUpdate['email_verified_at'])->toBeNull()
|
|
->and($emailUpdate)->not->toHaveKey('phone_verified_at');
|
|
|
|
$phoneUpdate = $service->clearVerificationForChangedContacts($subuser, [
|
|
'phone_country_code' => 46,
|
|
]);
|
|
expect($phoneUpdate['phone_verified_at'])->toBeNull()
|
|
->and($phoneUpdate)->not->toHaveKey('email_verified_at');
|
|
});
|
|
|
|
it('lets superusers set and clear contact verification state', function (): void {
|
|
$now = 1_800_000_000;
|
|
$subuser = subuser_contact_verification_test_subuser();
|
|
$service = new subuser_contact_verification_service(
|
|
redis: subuser_contact_verification_test_redis(),
|
|
timeProvider: fn () => $now
|
|
);
|
|
|
|
$verified = $service->setVerificationState($subuser, 'email', true, ['type' => 'user', 'id' => 99]);
|
|
expect($verified['status'])->toBe('verified')
|
|
->and($subuser->email_verified_at->value())->toBe(date('Y-m-d H:i:s', $now));
|
|
|
|
$unverified = $service->setVerificationState($subuser, 'email', false, ['type' => 'user', 'id' => 99]);
|
|
expect($unverified['status'])->toBe('unverified')
|
|
->and($subuser->email_verified_at->value())->toBeNull();
|
|
});
|
|
|
|
it('refuses to mark missing contact values as verified', function (): void {
|
|
$subuser = subuser_contact_verification_test_subuser([
|
|
'email' => null,
|
|
]);
|
|
$service = new subuser_contact_verification_service(redis: subuser_contact_verification_test_redis());
|
|
|
|
$result = $service->setVerificationState($subuser, 'email', true, ['type' => 'user', 'id' => 99]);
|
|
expect($result['status'])->toBe('missing_destination')
|
|
->and($subuser->email_verified_at->value())->toBeNull();
|
|
});
|