E-Mail-Retry, Voucher-Resend, Tageslimit, Docker-Politur
- Mailer::send mit Retry (2 Versuche); SMTP-Test & Templates waren bereits da - admin/vouchers.php: Code per E-Mail (erneut) versenden (ajax_resend) - Tageslimit Voucher pro Nicht-Admin-Benutzer (Setting + Durchsetzung in index) - Docker: HEALTHCHECK (health.php) + curl; GHCR-Publish-Workflow - Setting user_daily_voucher_limit + enforce in integrations.php
This commit is contained in:
parent
2e0f36d6c1
commit
997cda01a8
14 changed files with 350 additions and 8 deletions
35
tests/ApiKeyTest.php
Normal file
35
tests/ApiKeyTest.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
namespace Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once __DIR__ . '/../includes/ApiKey.php';
|
||||
|
||||
final class ApiKeyTest extends TestCase
|
||||
{
|
||||
public function testGenerateFormat(): void
|
||||
{
|
||||
$k = \ApiKey::generate();
|
||||
$this->assertStringStartsWith('uvt_', $k['plain']);
|
||||
$this->assertSame(44, strlen($k['plain']));
|
||||
$this->assertSame(hash('sha256', $k['plain']), $k['hash']);
|
||||
$this->assertSame(substr($k['plain'], 4, 8), $k['prefix']);
|
||||
}
|
||||
|
||||
public function testScopes(): void
|
||||
{
|
||||
$read = ['scope' => 'read'];
|
||||
$write = ['scope' => 'write'];
|
||||
$this->assertTrue(\ApiKey::hasScope($read, 'read'));
|
||||
$this->assertFalse(\ApiKey::hasScope($read, 'write'));
|
||||
$this->assertTrue(\ApiKey::hasScope($write, 'read'));
|
||||
$this->assertTrue(\ApiKey::hasScope($write, 'write'));
|
||||
}
|
||||
|
||||
public function testFromRequestBearer(): void
|
||||
{
|
||||
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer uvt_testkey123';
|
||||
$this->assertSame('uvt_testkey123', \ApiKey::fromRequest());
|
||||
unset($_SERVER['HTTP_AUTHORIZATION']);
|
||||
}
|
||||
}
|
||||
39
tests/CryptoTest.php
Normal file
39
tests/CryptoTest.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
namespace Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
if (!defined('APP_KEY')) {
|
||||
define('APP_KEY', base64_encode(random_bytes(32)));
|
||||
}
|
||||
require_once __DIR__ . '/../includes/Crypto.php';
|
||||
|
||||
final class CryptoTest extends TestCase
|
||||
{
|
||||
public function testRoundtrip(): void
|
||||
{
|
||||
$plain = 'geheim;mit"sonder@zeichen';
|
||||
$cipher = \Crypto::encrypt($plain);
|
||||
$this->assertNotSame($plain, $cipher);
|
||||
$this->assertTrue(\Crypto::isEncrypted($cipher));
|
||||
$this->assertSame($plain, \Crypto::decrypt($cipher));
|
||||
}
|
||||
|
||||
public function testPlaintextPassthrough(): void
|
||||
{
|
||||
// Legacy-/Klartextwerte werden unverändert zurückgegeben.
|
||||
$this->assertSame('altesKlartextPW', \Crypto::decrypt('altesKlartextPW'));
|
||||
}
|
||||
|
||||
public function testEmptyValues(): void
|
||||
{
|
||||
$this->assertSame('', \Crypto::encrypt(''));
|
||||
$this->assertNull(\Crypto::decrypt(null));
|
||||
}
|
||||
|
||||
public function testGenerateKeyLength(): void
|
||||
{
|
||||
$key = \Crypto::generateKey();
|
||||
$this->assertSame(32, strlen(base64_decode($key)));
|
||||
}
|
||||
}
|
||||
42
tests/MigrationSplitterTest.php
Normal file
42
tests/MigrationSplitterTest.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
namespace Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once __DIR__ . '/../updater/MigrationRunner.php';
|
||||
|
||||
final class MigrationSplitterTest extends TestCase
|
||||
{
|
||||
private function split(string $sql): array
|
||||
{
|
||||
$rc = new \ReflectionClass(\Updater\MigrationRunner::class);
|
||||
$inst = $rc->newInstanceWithoutConstructor();
|
||||
$m = $rc->getMethod('splitStatements');
|
||||
$m->setAccessible(true);
|
||||
$parts = $m->invoke($inst, $sql);
|
||||
return array_values(array_filter(array_map('trim', $parts), fn($s) => $s !== ''));
|
||||
}
|
||||
|
||||
public function testIgnoresSemicolonsInStringsAndComments(): void
|
||||
{
|
||||
$sql = "INSERT INTO t (a) VALUES (\";semi;colon\"); -- comment; not split\n"
|
||||
. "CREATE TABLE x (id INT); /* block ; comment */ INSERT INTO y VALUES (1);";
|
||||
$parts = $this->split($sql);
|
||||
$this->assertCount(3, $parts);
|
||||
}
|
||||
|
||||
public function testSingleStatement(): void
|
||||
{
|
||||
$parts = $this->split("ALTER TABLE users ADD COLUMN foo INT");
|
||||
$this->assertCount(1, $parts);
|
||||
}
|
||||
|
||||
public function testIgnorableErrorDetection(): void
|
||||
{
|
||||
$rc = new \ReflectionClass(\Updater\MigrationRunner::class);
|
||||
$inst = $rc->newInstanceWithoutConstructor();
|
||||
$this->assertTrue($inst->isIgnorableSqlError('Duplicate column name "x"', 'mysql'));
|
||||
$this->assertTrue($inst->isIgnorableSqlError('Table already exists', 'mysql'));
|
||||
$this->assertFalse($inst->isIgnorableSqlError('Syntax error near FROM', 'mysql'));
|
||||
}
|
||||
}
|
||||
46
tests/TotpTest.php
Normal file
46
tests/TotpTest.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
namespace Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once __DIR__ . '/../includes/Totp.php';
|
||||
|
||||
final class TotpTest extends TestCase
|
||||
{
|
||||
/** RFC 6238 Testvektoren (SHA1, 6 Stellen, Seed "12345678901234567890"). */
|
||||
public function testRfc6238Vectors(): void
|
||||
{
|
||||
$secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; // Base32 des RFC-Seeds
|
||||
$this->assertSame('287082', \Totp::code($secret, intdiv(59, 30)));
|
||||
$this->assertSame('081804', \Totp::code($secret, intdiv(1111111109, 30)));
|
||||
$this->assertSame('005924', \Totp::code($secret, intdiv(1234567890, 30)));
|
||||
}
|
||||
|
||||
public function testVerifyAcceptsCurrentCode(): void
|
||||
{
|
||||
$secret = \Totp::generateSecret();
|
||||
$code = \Totp::code($secret);
|
||||
$this->assertTrue(\Totp::verify($secret, $code));
|
||||
}
|
||||
|
||||
public function testVerifyRejectsWrongCode(): void
|
||||
{
|
||||
$secret = \Totp::generateSecret();
|
||||
$wrong = \Totp::code($secret) === '000000' ? '111111' : '000000';
|
||||
$this->assertFalse(\Totp::verify($secret, $wrong));
|
||||
}
|
||||
|
||||
public function testVerifyRejectsMalformed(): void
|
||||
{
|
||||
$secret = \Totp::generateSecret();
|
||||
$this->assertFalse(\Totp::verify($secret, 'abcdef'));
|
||||
$this->assertFalse(\Totp::verify($secret, '12345'));
|
||||
}
|
||||
|
||||
public function testProvisioningUri(): void
|
||||
{
|
||||
$uri = \Totp::provisioningUri('ABC', 'user@example.com', 'My App');
|
||||
$this->assertStringStartsWith('otpauth://totp/', $uri);
|
||||
$this->assertStringContainsString('secret=ABC', $uri);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue