407 lines
14 KiB
PHP
407 lines
14 KiB
PHP
<?php
|
|
namespace Tests\Unit\Domain\User;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Domain\User\UserRepository;
|
|
|
|
class UserRepositoryTest extends TestCase
|
|
{
|
|
public function testFindReturnsUserWhenExists(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('get')
|
|
->with('pp_users', '*', ['id' => 7])
|
|
->willReturn(['id' => 7, 'login' => 'admin']);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$user = $repository->find(7);
|
|
|
|
$this->assertIsArray($user);
|
|
$this->assertSame(7, (int)$user['id']);
|
|
$this->assertSame('admin', $user['login']);
|
|
}
|
|
|
|
public function testFindReturnsNullWhenNotFound(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('get')->willReturn(false);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertNull($repository->find(123));
|
|
}
|
|
|
|
public function testCheckLoginReturnsErrorWhenLoginIsTaken(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('get')
|
|
->with('pp_users', 'login', [
|
|
'AND' => [
|
|
'login' => 'taken@example.com',
|
|
'id[!]' => 10,
|
|
],
|
|
])
|
|
->willReturn('taken@example.com');
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$response = $repository->checkLogin('taken@example.com', 10);
|
|
|
|
$this->assertSame('error', $response['status']);
|
|
}
|
|
|
|
public function testCheckLoginReturnsOkWhenAvailable(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('get')->willReturn(null);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$response = $repository->checkLogin('free@example.com', 5);
|
|
|
|
$this->assertSame('ok', $response['status']);
|
|
}
|
|
|
|
public function testSaveReturnsErrorForTooShortPasswordOnCreate(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$repository = new UserRepository($mockDb);
|
|
|
|
$response = $repository->save(0, 'admin@example.com', 'on', '1234', '1234', 1, 'on', 'admin@example.com');
|
|
|
|
$this->assertSame('error', $response['status']);
|
|
}
|
|
|
|
public function testSaveReturnsErrorForMismatchedPasswordsOnCreate(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$repository = new UserRepository($mockDb);
|
|
|
|
$response = $repository->save(0, 'admin@example.com', 'on', 'password1', 'password2', 1);
|
|
|
|
$this->assertSame('error', $response['status']);
|
|
$this->assertStringContainsString('rozne', $response['msg']);
|
|
}
|
|
|
|
public function testSaveCreatesUserWithNormalizedSwitches(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('insert')
|
|
->with('pp_users', $this->callback(function (array $row): bool {
|
|
return $row['login'] === 'new@example.com'
|
|
&& $row['status'] === 1
|
|
&& $row['admin'] === 1
|
|
&& $row['twofa_enabled'] === 1
|
|
&& $row['twofa_email'] === '2fa@example.com'
|
|
&& $row['password'] === md5('secret5');
|
|
}))
|
|
->willReturn($this->createMock(\PDOStatement::class));
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$response = $repository->save(0, 'new@example.com', 'on', 'secret5', 'secret5', 1, 'on', '2fa@example.com');
|
|
|
|
$this->assertSame('ok', $response['status']);
|
|
}
|
|
|
|
public function testSaveUpdatesExistingUserWithPassword(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->exactly(2))
|
|
->method('update')
|
|
->withConsecutive(
|
|
[
|
|
'pp_users',
|
|
$this->callback(fn(array $d) => $d['password'] === md5('newpass5')),
|
|
['id' => 5],
|
|
],
|
|
[
|
|
'pp_users',
|
|
$this->callback(fn(array $d) => $d['login'] === 'user@example.com' && $d['status'] === 1),
|
|
['id' => 5],
|
|
]
|
|
);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$response = $repository->save(5, 'user@example.com', 'on', 'newpass5', 'newpass5', 1);
|
|
|
|
$this->assertSame('ok', $response['status']);
|
|
}
|
|
|
|
public function testSaveUpdatesExistingUserWithoutPassword(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('update')
|
|
->with(
|
|
'pp_users',
|
|
$this->callback(fn(array $d) => $d['login'] === 'user@example.com'),
|
|
['id' => 5]
|
|
);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$response = $repository->save(5, 'user@example.com', 'on', '', '', 1);
|
|
|
|
$this->assertSame('ok', $response['status']);
|
|
}
|
|
|
|
public function testSaveReturnsErrorForTooShortPasswordOnUpdate(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$repository = new UserRepository($mockDb);
|
|
|
|
$response = $repository->save(5, 'user@example.com', 1, '123', '123', 1);
|
|
|
|
$this->assertSame('error', $response['status']);
|
|
}
|
|
|
|
public function testSaveReturnsErrorForMismatchedPasswordsOnUpdate(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$repository = new UserRepository($mockDb);
|
|
|
|
$response = $repository->save(5, 'user@example.com', 1, 'password1', 'password2', 1);
|
|
|
|
$this->assertSame('error', $response['status']);
|
|
}
|
|
|
|
public function testDeleteReturnsTrue(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('delete')
|
|
->with('pp_users', ['id' => 3])
|
|
->willReturn($this->createMock(\PDOStatement::class));
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertTrue($repository->delete(3));
|
|
}
|
|
|
|
public function testDeleteReturnsFalseOnFailure(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('delete')->willReturn(false);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertFalse($repository->delete(999));
|
|
}
|
|
|
|
public function testDetailsReturnsUserByLogin(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('get')
|
|
->with('pp_users', '*', ['login' => 'admin@shop.com'])
|
|
->willReturn(['id' => 1, 'login' => 'admin@shop.com']);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$user = $repository->details('admin@shop.com');
|
|
|
|
$this->assertIsArray($user);
|
|
$this->assertSame('admin@shop.com', $user['login']);
|
|
}
|
|
|
|
public function testDetailsReturnsNullWhenNotFound(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('get')->willReturn(false);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertNull($repository->details('nonexistent@shop.com'));
|
|
}
|
|
|
|
public function testLogonReturnsSuccessForValidCredentials(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->expects($this->exactly(3))
|
|
->method('get')
|
|
->withConsecutive(
|
|
['pp_users', '*', ['login' => 'admin@example.com']],
|
|
['pp_users', '*', ['AND' => ['login' => 'admin@example.com', 'status' => 1, 'error_logged_count[<]' => 5]]],
|
|
['pp_users', '*', ['AND' => ['login' => 'admin@example.com', 'status' => 1, 'password' => md5('password123')]]]
|
|
)
|
|
->willReturnOnConsecutiveCalls(
|
|
['id' => 5],
|
|
['id' => 5],
|
|
['id' => 5]
|
|
);
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('update')
|
|
->with(
|
|
'pp_users',
|
|
$this->callback(function (array $data): bool {
|
|
return isset($data['last_logged']) && $data['error_logged_count'] === 0;
|
|
}),
|
|
['login' => 'admin@example.com']
|
|
);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$result = $repository->logon('admin@example.com', 'password123');
|
|
|
|
$this->assertSame(1, $result);
|
|
}
|
|
|
|
public function testLogonReturnsZeroForNonexistentUser(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('get')
|
|
->with('pp_users', '*', ['login' => 'noone@example.com'])
|
|
->willReturn(false);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertSame(0, $repository->logon('noone@example.com', 'pass'));
|
|
}
|
|
|
|
public function testLogonReturnsNegativeOneForBlockedUser(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->exactly(2))
|
|
->method('get')
|
|
->withConsecutive(
|
|
['pp_users', '*', ['login' => 'blocked@example.com']],
|
|
['pp_users', '*', ['AND' => ['login' => 'blocked@example.com', 'status' => 1, 'error_logged_count[<]' => 5]]]
|
|
)
|
|
->willReturnOnConsecutiveCalls(
|
|
['id' => 3, 'status' => 0],
|
|
false
|
|
);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertSame(-1, $repository->logon('blocked@example.com', 'pass'));
|
|
}
|
|
|
|
public function testVerifyTwofaCodeReturnsFalseForNonexistentUser(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('get')->willReturn(false);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertFalse($repository->verifyTwofaCode(999, '123456'));
|
|
}
|
|
|
|
public function testVerifyTwofaCodeReturnsFalseAfterMaxAttempts(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('get')->willReturn([
|
|
'id' => 1,
|
|
'twofa_failed_attempts' => 5,
|
|
'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
|
|
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 300),
|
|
]);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertFalse($repository->verifyTwofaCode(1, '123456'));
|
|
}
|
|
|
|
public function testVerifyTwofaCodeReturnsFalseForExpiredCode(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('get')->willReturn([
|
|
'id' => 1,
|
|
'twofa_failed_attempts' => 0,
|
|
'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
|
|
'twofa_expires_at' => date('Y-m-d H:i:s', time() - 60),
|
|
]);
|
|
|
|
$mockDb->expects($this->once())->method('update');
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertFalse($repository->verifyTwofaCode(1, '123456'));
|
|
}
|
|
|
|
public function testVerifyTwofaCodeReturnsTrueForValidCode(): void
|
|
{
|
|
$code = '654321';
|
|
$hash = password_hash($code, PASSWORD_DEFAULT);
|
|
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('get')->willReturn([
|
|
'id' => 1,
|
|
'twofa_failed_attempts' => 0,
|
|
'twofa_code_hash' => $hash,
|
|
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 300),
|
|
]);
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('update')
|
|
->with('pp_users', $this->callback(function (array $d): bool {
|
|
return $d['twofa_code_hash'] === null
|
|
&& $d['twofa_expires_at'] === null
|
|
&& $d['twofa_failed_attempts'] === 0;
|
|
}), ['id' => 1]);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertTrue($repository->verifyTwofaCode(1, $code));
|
|
}
|
|
|
|
public function testSendTwofaCodeReturnsFalseWhen2FADisabled(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('get')->willReturn([
|
|
'id' => 1,
|
|
'twofa_enabled' => 0,
|
|
'login' => 'test@example.com',
|
|
]);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertFalse($repository->sendTwofaCode(1));
|
|
}
|
|
|
|
public function testSendTwofaCodeReturnsFalseForInvalidEmail(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->method('get')->willReturn([
|
|
'id' => 1,
|
|
'twofa_enabled' => 1,
|
|
'twofa_email' => 'not-an-email',
|
|
'login' => 'also-not-email',
|
|
]);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertFalse($repository->sendTwofaCode(1));
|
|
}
|
|
|
|
public function testUpdateByIdCallsDbUpdate(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('update')
|
|
->with('pp_users', ['status' => 0], ['id' => 7])
|
|
->willReturn($this->createMock(\PDOStatement::class));
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$this->assertTrue($repository->updateById(7, ['status' => 0]));
|
|
}
|
|
|
|
public function testListForAdminReturnsItemsAndTotal(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$countStmt = $this->createMock(\PDOStatement::class);
|
|
$countStmt->expects($this->once())
|
|
->method('fetchAll')
|
|
->willReturn([[2]]);
|
|
|
|
$dataStmt = $this->createMock(\PDOStatement::class);
|
|
$dataStmt->expects($this->once())
|
|
->method('fetchAll')
|
|
->willReturn([
|
|
['id' => 2, 'login' => 'a@example.com', 'status' => 1],
|
|
['id' => 3, 'login' => 'b@example.com', 'status' => 0],
|
|
]);
|
|
|
|
$mockDb->expects($this->exactly(2))
|
|
->method('query')
|
|
->willReturnOnConsecutiveCalls($countStmt, $dataStmt);
|
|
|
|
$repository = new UserRepository($mockDb);
|
|
$result = $repository->listForAdmin(['login' => '', 'status' => ''], 'login', 'ASC', 1, 15);
|
|
|
|
$this->assertSame(2, $result['total']);
|
|
$this->assertCount(2, $result['items']);
|
|
$this->assertSame('a@example.com', $result['items'][0]['login']);
|
|
}
|
|
}
|