Files
xamxam/tests/phpunit/FileAccessTokenTest.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']);
}
}