test: add characterization tests for file-access token lifecycle

This commit is contained in:
Pontoporeia committed 2026-08-24 11:36:02 +02:00
1 parent abb735253e
commit 3f1dcf5d43
2 files changed
+152 -1

No files matched your search

+1 -1
View File
@@ -1 +1 @@
{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":7,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":7,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":7,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":7,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":7,"RateLimitExtendedTest::testGetRemainingDecrements":7,"RateLimitExtendedTest::testGetRemainingAtLimit":7,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":7,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":7,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":8,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForHexId":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForPeertubeVideo":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForPeertubeAudio":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForMixedIds":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForHexInCoverQueue":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForPeertubeInCoverQueue":8,"PureLogicTest::testHasFilePondQueueDataReturnsFalseForNumericIdsOnly":8,"PureLogicTest::testHasFilePondQueueDataReturnsFalseForEmptyInput":8,"PureLogicTest::testHasFilePondQueueDataReturnsFalseForEmptyStrings":8,"PureLogicTest::testHasFilePondQueueDataHandlesScalarNotArray":8},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0,"CryptoTest::testEncryptDecryptWithUnicode":0,"CryptoTest::testEncryptDecryptMultiline":0,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0,"CryptoTest::testIsEncryptedRejectsPlaintext":0,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0,"EmailObfuscatorTest::testMultipleEmailsInOneString":0,"EmailObfuscatorTest::testEmailWithPlusSign":0,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0,"StudentEmailTest::testBuildHtmlContainsKeyFields":0,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0,"StudentEmailTest::testBuildHtmlContainsLabelFields":0,"SystemControllerHelpersTest::testHumanBytesZero":0,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneMB":0,"SystemControllerHelpersTest::testHumanBytesOneGB":0,"SystemControllerHelpersTest::testHumanBytes1523MB":0,"SystemControllerHelpersTest::testHumanBytes2500GB":0,"SystemControllerHelpersTest::testDiskColorBelowWarning":0,"SystemControllerHelpersTest::testDiskColorWarning":0,"SystemControllerHelpersTest::testDiskColorCritical":0,"SystemControllerHelpersTest::testLogLineClassCrit":0,"SystemControllerHelpersTest::testLogLineClassError":0,"SystemControllerHelpersTest::testLogLineClassWarn":0,"SystemControllerHelpersTest::testLogLineClassNotice":0,"SystemControllerHelpersTest::testLogLineClassHttp500":0,"SystemControllerHelpersTest::testLogLineClassHttp300":0,"SystemControllerHelpersTest::testLogLineClassDefault":0,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemCoLine truncated
{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":7,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":7,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":7,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":7,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":7,"RateLimitExtendedTest::testGetRemainingDecrements":7,"RateLimitExtendedTest::testGetRemainingAtLimit":7,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":7,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":7,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":8,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForHexId":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForPeertubeVideo":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForPeertubeAudio":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForMixedIds":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForHexInCoverQueue":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForPeertubeInCoverQueue":8,"PureLogicTest::testHasFilePondQueueDataReturnsFalseForNumericIdsOnly":8,"PureLogicTest::testHasFilePondQueueDataReturnsFalseForEmptyInput":8,"PureLogicTest::testHasFilePondQueueDataReturnsFalseForEmptyStrings":8,"PureLogicTest::testHasFilePondQueueDataHandlesScalarNotArray":8},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0,"CryptoTest::testEncryptDecryptWithUnicode":0,"CryptoTest::testEncryptDecryptMultiline":0,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0,"CryptoTest::testIsEncryptedRejectsPlaintext":0,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0,"EmailObfuscatorTest::testMultipleEmailsInOneString":0,"EmailObfuscatorTest::testEmailWithPlusSign":0,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0,"StudentEmailTest::testBuildHtmlContainsKeyFields":0,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0,"StudentEmailTest::testBuildHtmlContainsLabelFields":0,"SystemControllerHelpersTest::testHumanBytesZero":0,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneMB":0,"SystemControllerHelpersTest::testHumanBytesOneGB":0,"SystemControllerHelpersTest::testHumanBytes1523MB":0,"SystemControllerHelpersTest::testHumanBytes2500GB":0,"SystemControllerHelpersTest::testDiskColorBelowWarning":0,"SystemControllerHelpersTest::testDiskColorWarning":0,"SystemControllerHelpersTest::testDiskColorCritical":0,"SystemControllerHelpersTest::testLogLineClassCrit":0,"SystemControllerHelpersTest::testLogLineClassError":0,"SystemControllerHelpersTest::testLogLineClassWarn":0,"SystemControllerHelpersTest::testLogLineClassNotice":0,"SystemControllerHelpersTest::testLogLineClassHttp500":0,"SystemControllerHelpersTest::testLogLineClassHttp300":0,"SystemControllerHelpersTest::testLogLineClassDefault":0,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemCoLine truncated
+151
View File
@@ -0,0 +1,151 @@
<?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 file_access_tokens WHERE request_id = ?');
$stmt->execute([$requestId]);
$row = $stmt->fetch();
$this->assertNotFalse($row);
$this->assertSame($token, $row['token'], 'Current impl stores the plaintext token');
$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 file_access_tokens WHERE request_id = ?');
$stmt->execute([$requestId]);
$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']);
}
}