mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 09:53:08 +02:00
158 lines
5.8 KiB
PHP
158 lines
5.8 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* FileAccessTokenTest — characterization tests for the file-access one-time
|
|
* token lifecycle (Database::generateAccessToken / redeemAccessToken).
|
|
*
|
|
* These pin the CURRENT contract so we can migrate the implementation onto
|
|
* the shared OneTimeToken model without changing behavior (behaviour-
|
|
* preserving refactor). Run before and after the migration; results must
|
|
* stay green.
|
|
*/
|
|
class FileAccessTokenTest extends TestCase
|
|
{
|
|
private Database $db;
|
|
private PDO $pdo;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
TestDatabase::resetData();
|
|
$this->db = TestDatabase::getInstance();
|
|
$this->pdo = TestDatabase::getPDO();
|
|
}
|
|
|
|
/**
|
|
* Seed a thesis + an approved file-access request, returning
|
|
* [thesisId, requestId].
|
|
*/
|
|
private function seedApprovedRequest(): array
|
|
{
|
|
[$authorId, $thesisId] = TestDatabase::seedBasicThesis();
|
|
$requestId = $this->db->createFileAccessRequest($thesisId, 'xamxam@erg.be', null);
|
|
// Approve it (status must be 'approved' for redemption to succeed).
|
|
$this->pdo->prepare(
|
|
"UPDATE file_access_requests SET status = 'approved', approved_at = CURRENT_TIMESTAMP WHERE id = ?"
|
|
)->execute([$requestId]);
|
|
return [$thesisId, $requestId];
|
|
}
|
|
|
|
// ── generateAccessToken ──────────────────────────────────────────────────
|
|
|
|
public function testGenerateAccessTokenReturns256BitHex(): void
|
|
{
|
|
[$thesisId, $requestId] = $this->seedApprovedRequest();
|
|
|
|
$token = $this->db->generateAccessToken($requestId, 24);
|
|
|
|
$this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $token);
|
|
}
|
|
|
|
public function testGenerateAccessTokenPersistsRowWithExpectedState(): void
|
|
{
|
|
[$thesisId, $requestId] = $this->seedApprovedRequest();
|
|
|
|
$token = $this->db->generateAccessToken($requestId, 24);
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
"SELECT * FROM one_time_tokens WHERE purpose = 'file_access' ORDER BY id DESC LIMIT 1"
|
|
);
|
|
$stmt->execute();
|
|
$row = $stmt->fetch();
|
|
|
|
$this->assertNotFalse($row);
|
|
$this->assertSame(hash('sha256', $token), $row['token_hash'], 'Only the hash is stored, never the plaintext');
|
|
$this->assertNotSame($token, $row['token_hash']);
|
|
$this->assertSame(['request_id' => $requestId], json_decode($row['context'], true));
|
|
$this->assertSame(1, (int) $row['is_valid']);
|
|
$this->assertNull($row['used_at']);
|
|
$this->assertNotNull($row['expires_at']);
|
|
}
|
|
|
|
// ── redeemAccessToken ────────────────────────────────────────────────────
|
|
|
|
public function testRedeemValidTokenReturnsThesisAndRequestIds(): void
|
|
{
|
|
[$thesisId, $requestId] = $this->seedApprovedRequest();
|
|
$token = $this->db->generateAccessToken($requestId, 24);
|
|
|
|
$result = $this->db->redeemAccessToken($token, '1.2.3.4', 'ua');
|
|
|
|
$this->assertNotNull($result);
|
|
$this->assertSame($thesisId, $result['thesis_id']);
|
|
$this->assertSame($requestId, $result['request_id']);
|
|
}
|
|
|
|
public function testRedeemMarksTokenUsed(): void
|
|
{
|
|
[$thesisId, $requestId] = $this->seedApprovedRequest();
|
|
$token = $this->db->generateAccessToken($requestId, 24);
|
|
|
|
$this->db->redeemAccessToken($token, '1.2.3.4', 'ua');
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
"SELECT used_at, is_valid FROM one_time_tokens WHERE purpose = 'file_access'"
|
|
);
|
|
$stmt->execute();
|
|
$row = $stmt->fetch();
|
|
|
|
$this->assertNotNull($row['used_at']);
|
|
$this->assertSame(0, (int) $row['is_valid']);
|
|
}
|
|
|
|
public function testRedeemIsOneTimeUse(): void
|
|
{
|
|
[$thesisId, $requestId] = $this->seedApprovedRequest();
|
|
$token = $this->db->generateAccessToken($requestId, 24);
|
|
|
|
$first = $this->db->redeemAccessToken($token, '1.2.3.4', 'ua');
|
|
$second = $this->db->redeemAccessToken($token, '1.2.3.4', 'ua');
|
|
|
|
$this->assertNotNull($first);
|
|
$this->assertNull($second);
|
|
}
|
|
|
|
public function testRedeemUnknownTokenReturnsNull(): void
|
|
{
|
|
$result = $this->db->redeemAccessToken(str_repeat('ab', 32), '1.2.3.4', 'ua');
|
|
$this->assertNull($result);
|
|
}
|
|
|
|
public function testRedeemExpiredTokenReturnsNull(): void
|
|
{
|
|
[$thesisId, $requestId] = $this->seedApprovedRequest();
|
|
// Negative hours → already expired
|
|
$token = $this->db->generateAccessToken($requestId, -1);
|
|
|
|
$result = $this->db->redeemAccessToken($token, '1.2.3.4', 'ua');
|
|
$this->assertNull($result);
|
|
}
|
|
|
|
public function testRedeemLogsAuditOnSuccess(): void
|
|
{
|
|
[$thesisId, $requestId] = $this->seedApprovedRequest();
|
|
$token = $this->db->generateAccessToken($requestId, 24);
|
|
|
|
$this->db->redeemAccessToken($token, '1.2.3.4', 'ua');
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
"SELECT COUNT(*) FROM file_access_audit WHERE request_id = ? AND event = 'redeemed'"
|
|
);
|
|
$stmt->execute([$requestId]);
|
|
$this->assertSame(1, (int) $stmt->fetchColumn());
|
|
}
|
|
|
|
public function testRedeemDoesNotRequireThesisIdArgument(): void
|
|
{
|
|
// The thesis is resolved from the request JOIN, not passed in.
|
|
[$thesisId, $requestId] = $this->seedApprovedRequest();
|
|
$token = $this->db->generateAccessToken($requestId, 24);
|
|
|
|
// Approving a different request must not affect this one's scoping —
|
|
// the returned thesis_id is derived from file_access_requests.
|
|
$result = $this->db->redeemAccessToken($token, '1.2.3.4', 'ua');
|
|
$this->assertSame($thesisId, $result['thesis_id']);
|
|
}
|
|
}
|