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']); } }