Files
ops-Gazelle/tests/phpunit/ReaperTest.php

593 lines
26 KiB
PHP

<?php
use \PHPUnit\Framework\TestCase;
/**
* Note to Developers/Testers
* ==========================
*
* In the context of a local development environment, this unit
* test is fairly sensitive to parameters outside its control.
* Tests may fail because of _other_ unseeded/never seeded uploads
* that you have created locally.
*
* If you receive errors (notably never-initial-0-count or
* unseeded-initial-0-count) the tests are likely catching your
* uploads in its net. To work around this:
*
* UPDATE torrents_leech_stats SET last_action = now()
*
* will make the tests happy and everything should succeed. Any
* other problems are your own.
*/
require_once(__DIR__ . '/../../lib/bootstrap.php');
require_once(__DIR__ . '/../helper.php');
class ReaperTest extends TestCase {
protected string $tgroupName;
protected array $torrentList = [];
protected array $userList = [];
public function setUp(): void {
// we need two users, one who uploads and one who snatches
$this->userList = [
Helper::makeUser('reaper.' . randomString(10), 'reaper'),
Helper::makeUser('reaper.' . randomString(10), 'reaper'),
];
// enable them and wipe their inboxes (there is only one message)
foreach ($this->userList as $user) {
$user->setField('Enabled', '1')->modify();
$pmMan = new Gazelle\Manager\PM($user);
foreach ((new Gazelle\User\Inbox($user))->messageList($pmMan, 1, 0) as $pm) {
$pm->remove();
}
}
// create a torrent group
$this->tgroupName = 'phpunit reaper ' . randomString(6);
$tgroup = Helper::makeTGroupMusic(
name: $this->tgroupName,
artistName: [[ARTIST_MAIN], ['Reaper Girl ' . randomString(12)]],
tagName: ['electronic'],
user: $this->userList[0],
);
// and add some torrents to the group
$this->torrentList = array_map(fn($info) =>
Helper::makeTorrentMusic(
tgroup: $tgroup,
user: $this->userList[0],
title: $info['title'],
), [
['title' => 'Deluxe Edition'],
['title' => 'Limited Edition'],
]
);
}
public function tearDown(): void {
$this->removeUnseededAlert($this->torrentList);
Helper::removeTGroup($this->torrentList[0]->group(), $this->userList[0]);
foreach ($this->userList as $user) {
$user->remove();
}
}
// --- HELPER FUNCTIONS ----
protected function generateReseed(Gazelle\Torrent $torrent, Gazelle\User $user): void {
$db = Gazelle\DB::DB();
$db->prepared_query("
UPDATE torrents_leech_stats SET last_action = now() WHERE TorrentID = ?
", $torrent->id()
);
$db->prepared_query("
INSERT INTO xbt_files_users
(fid, uid, useragent, peer_id, active, remaining, ip, timespent, mtime)
VALUES (?, ?, ?, ?, 1, 0, '127.0.0.1', 1, unix_timestamp(now() - interval 1 hour))
", $torrent->id(), $user->id(), 'ua-' . randomString(12), randomString(20)
);
}
protected function generateSnatch(Gazelle\Torrent $torrent, Gazelle\User $user): void {
Gazelle\DB::DB()->prepared_query("
INSERT INTO xbt_snatched
(fid, uid, tstamp, IP, seedtime)
VALUES (?, ?, unix_timestamp(now()), '127.0.0.1', 1)
", $torrent->id(), $user->id()
);
}
protected function modifyLastAction(Gazelle\Torrent $torrent, int $interval): void {
Gazelle\DB::DB()->prepared_query("
UPDATE torrents_leech_stats SET
last_action = now() - INTERVAL ? HOUR
WHERE TorrentID = ?
", $interval, $torrent->id()
);
}
protected function modifyUnseededInterval(Gazelle\Torrent $torrent, int $hour): void {
Gazelle\DB::DB()->prepared_query("
UPDATE torrent_unseeded SET
unseeded_date = ?
WHERE torrent_id = ?
", date('Y-m-d H:i:s', (int)strtotime("-{$hour} hours")), $torrent->id()
);
}
protected function removeUnseededAlert(array $list): void {
Gazelle\DB::DB()->prepared_query("
DELETE FROM torrent_unseeded WHERE torrent_id in (" . placeholders($list) . ")"
, ...array_map(fn($t) => $t->id(), $list)
);
}
/**
* This method is not necessary per se, but came in handy when debugging the tests
*/
protected function status(array $torrentList): void {
$db = Gazelle\DB::DB();
$db->prepared_query("
SELECT t.ID,
t.created,
tu.unseeded_date < tls.last_action,
coalesce(tls.last_action, 'null'),
coalesce(tu.unseeded_date, 'null'),
coalesce(tu.notify, 'null'),
coalesce(tu.state, 'null')
FROM torrents t
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID)
LEFT JOIN torrent_unseeded tu ON (tu.torrent_id = t.ID)
WHERE t.ID IN (" . placeholders($torrentList) . ")"
, ...array_map(fn($t) => $t->id(), $torrentList)
);
echo implode("\t", ['id', 'created', 'created<last?', 'last_action', 'unseeded', 'final', 'never_seeded']), "\n";
echo implode("\n",
array_map(fn($r) => implode("\t", $r), $db->to_array(false, MYSQLI_NUM, false))
) . "\n";
}
// -------------------------
public function testExpand(): void {
$reaper = new \Gazelle\Torrent\Reaper(new Gazelle\Manager\Torrent, new Gazelle\Manager\User);
$this->assertEquals(
[456 => [99, 88, 77]],
$reaper->expand(100, [[456, '99,88,77']]),
'torrent-reaper-expand-1',
);
$this->assertEquals(
[
456 => [99, 88, 77],
567 => [10, 20, 30, 40, 50],
678 => [321],
],
$reaper->expand(100, [
[456, '99,88,77'],
[567, '10,20,30,40,50'],
[678, '321'],
]), 'torrent-reaper-expand-2',
);
$this->assertEquals(
[
456 => [99, 88],
567 => [10, 20],
678 => [321],
],
$reaper->expand(2, [
[456, '99,88,77'],
[567, '10,20,30,40,50'],
[678, '321'],
]),
'torrent-reaper-expand-limit',
);
}
public function testNeverSeeded(): void {
$user = $this->torrentList[0]->uploader();
$pmMan = new Gazelle\Manager\PM($user);
$inbox = new Gazelle\User\Inbox($user);
$this->assertEquals(0, $inbox->messageTotal(), 'never-inbox-initial');
$torMan = new Gazelle\Manager\Torrent;
$userMan = new Gazelle\Manager\User;
$reaper = new \Gazelle\Torrent\Reaper($torMan, $userMan);
$initialUnseededStats = $reaper->stats();
// reset the time of the never seeded alert back in time to hit the initial timeout
$hour = NOTIFY_NEVER_SEEDED_INITIAL_HOUR + 1;
$neverSeededInitialDate = date('Y-m-d H:i:s', strtotime("-{$hour} hours"));
$this->torrentList[0]->setField('created', $neverSeededInitialDate)->modify();
// look for never seeded
$neverInitial = $reaper->initialNeverSeededList();
$this->assertCount(1, $neverInitial, 'never-initial-0-count');
$this->assertEquals(
1,
$reaper->process(
$neverInitial,
Gazelle\Torrent\ReaperState::NEVER,
Gazelle\Torrent\ReaperNotify::INITIAL
),
'never-initial-process'
);
$this->assertEquals(
[
"never_seeded_initial" => $initialUnseededStats['never_seeded_initial'] + 1,
"never_seeded_final" => 0,
"unseeded_initial" => 0,
"unseeded_final" => 0,
],
$reaper->stats(),
'reaper-unseeded-stats-0'
);
// check the notification
$this->assertEquals(1, $inbox->messageTotal(), 'never-message-initial-count');
$pm = $inbox->messageList($pmMan, 1, 0)[0];
$this->assertEquals('You have a non-seeded new upload to rescue', $pm->subject(), 'never-message-initial-subject');
$pm->remove();
// reset the unseeded entries and time out a second upload
$this->removeUnseededAlert($this->torrentList);
$this->torrentList[1]->setField('created', $neverSeededInitialDate)->modify();
$neverInitial = $reaper->initialNeverSeededList();
$this->assertCount(1, $neverInitial, 'never-initial-2'); // one user ...
$this->assertEquals(2, // ... with two uploads
$reaper->process(
$neverInitial,
Gazelle\Torrent\ReaperState::NEVER,
Gazelle\Torrent\ReaperNotify::INITIAL
),
'never-2-process'
);
$this->assertEquals(
[
"never_seeded_initial" => $initialUnseededStats['never_seeded_initinal'] + 2,
"never_seeded_final" => 0,
"unseeded_initial" => 0,
"unseeded_final" => 0,
],
$reaper->stats(),
'reaper-unseeded-stats-1'
);
$this->assertEquals(1, $inbox->messageTotal(), 'never-message-2');
$pm = $inbox->messageList($pmMan, 1, 0)[0];
$body = $pm->postList(1, 0)[0]['body'];
$this->assertEquals('You have 2 non-seeded new uploads to rescue', $pm->subject(), 'never-message-2');
$this->assertStringContainsString("Dear {$this->userList[0]->username()}", $body, 'never-body-2-dear');
$this->assertStringContainsString("[pl]{$this->torrentList[0]->id()}[/pl]", $body, 'never-body-2-pl-0');
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'never-body-2-pl-1');
$pm->remove();
// reseed one of the torrents by the uploader
$this->generateReseed($this->torrentList[0], $this->torrentList[0]->uploader());
$this->torrentList[1]->setField('created', date('Y-m-d H:i:s'))->modify();
// reset the time of the remaing never seeded alert back in time to hit
// the final timeout.
// reminder: once a torrent is referenced in the torrent_unseeded table,
// we no longer need to consider the torrent creation date.
$this->modifyUnseededInterval($this->torrentList[1], NOTIFY_NEVER_SEEDED_FINAL_HOUR + 1);
$this->assertEquals(0, $inbox->messageTotal(), 'never-message-final-none');
$neverFinal = $reaper->finalNeverSeededList();
$this->assertCount(1, $neverFinal, 'never-final-0'); // one user ...
$this->assertEquals(1, // ... with one upload
$reaper->process(
$neverFinal,
Gazelle\Torrent\ReaperState::NEVER,
Gazelle\Torrent\ReaperNotify::FINAL
),
'never-final-process'
);
$this->assertEquals(
[
"never_seeded_initial" => $initialUnseededStats['never_seeded_initial'] + 1,
"never_seeded_final" => $initialUnseededStats['never_seeded_final'] + 1,
"unseeded_initial" => 0,
"unseeded_final" => 0,
],
$reaper->stats(),
'reaper-unseeded-stats-final'
);
$this->assertEquals(1, $inbox->messageTotal(), 'never-message-final-count');
$pm = $inbox->messageList($pmMan, 1, 0)[0];
$body = $pm->postList(1, 0)[0]['body'];
$this->assertEquals('You have a non-seeded new upload scheduled for deletion very soon', $pm->subject(), 'never-message-final-subject');
$this->assertStringContainsString("Dear {$this->userList[0]->username()}", $body, 'never-body-3-dear');
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'never-body-3-pl-1');
$pm->remove();
$this->modifyUnseededInterval($this->torrentList[1], REMOVE_NEVER_SEEDED_HOUR + 1);
$id = $this->torrentList[1]->id();
$list = $reaper->reaperList(
state: Gazelle\Torrent\ReaperState::NEVER,
interval: REMOVE_NEVER_SEEDED_HOUR,
);
$this->assertCount(1, $list, 'never-reap-list');
$this->assertEquals(1, $reaper->removeNeverSeeded(), 'never-reap-remove');
$pm = $inbox->messageList($pmMan, 1, 0)[0];
$body = $pm->postList(1, 0)[0]['body'];
$this->assertEquals('1 of your uploads has been deleted for inactivity (never seeded)', $pm->subject(), 'never-remove-message');
$this->assertStringContainsString("Dear {$this->userList[0]->username()}", $body, 'never-remove-body-dear');
$this->assertStringContainsString("[url=torrents.php?id={$this->torrentList[1]->group()->id()}]", $body, 'never-remove-body-pl');
$pm->remove();
$deleted = $torMan->findById($id);
$this->assertNull($deleted, 'never-was-reaped');
}
public function testUnseeded(): void {
$torMan = new Gazelle\Manager\Torrent;
$userMan = new Gazelle\Manager\User;
$reaper = new \Gazelle\Torrent\Reaper($torMan, $userMan);
$initialUnseededStats = $reaper->stats();
$unseededInitial = $reaper->initialUnseededList();
$this->assertCount(0, $unseededInitial, 'unseeded-initial-0-count');
// reset the last action and time of the unseeded alert back in time to hit the initial timeout
foreach ($this->torrentList as $torrent) {
$hour = NOTIFY_UNSEEDED_INITIAL_HOUR + 1;
$torrent->setField('created', date('Y-m-d H:i:s', strtotime("-{$hour} hours")))->modify();
$this->modifyLastAction($torrent, NOTIFY_UNSEEDED_INITIAL_HOUR + 2);
// pretend they were snatched
foreach ($this->userList as $user) {
$this->generateSnatch($torrent, $user);
}
}
// look for unseeded
$unseededInitial = $reaper->initialUnseededList();
$this->assertCount(1, $unseededInitial, 'unseeded-initial-1');
$this->assertEquals(
2,
$reaper->process(
$unseededInitial,
Gazelle\Torrent\ReaperState::UNSEEDED,
Gazelle\Torrent\ReaperNotify::INITIAL
),
'unseeded-initial-process'
);
$this->assertEquals(
[
"never_seeded_initial" => 0,
"never_seeded_final" => 0,
"unseeded_initial" => $initialUnseededStats['unseeded_seeded_initial'] + 2,
"unseeded_final" => 0,
],
$reaper->stats(),
'reaper-unseeded-stats-0'
);
$inboxList = [
new Gazelle\User\Inbox($this->userList[0]),
new Gazelle\User\Inbox($this->userList[1]),
];
$pmMan = [
new Gazelle\Manager\PM($this->userList[0]),
new Gazelle\Manager\PM($this->userList[1]),
];
$this->assertEquals(1, $inboxList[0]->messageTotal(), 'unseeded-initial-0-inbox');
$pm = $inboxList[0]->messageList($pmMan[0], 1, 0)[0];
$body = $pm->postList(1, 0)[0]['body'];
$this->assertEquals('There are 2 unseeded uploads to rescue', $pm->subject(), 'unseeded-initial-0-rescue');
$this->assertStringContainsString("You have 2 uploads that are not currently seeding by you (or anyone else)", $body, 'unseeded-initial-0-body');
$this->assertStringContainsString("[pl]{$this->torrentList[0]->id()}[/pl]", $body, 'unseeded-initial-0-body-pl-0');
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'unseeded-initial-0-body-pl-1');
$pm->remove();
$this->assertEquals(1, $inboxList[1]->messageTotal(), 'unseeded-initial-1-inbox');
$pm = $inboxList[1]->messageList($pmMan[1], 1, 0)[0];
$body = $pm->postList(1, 0)[0]['body'];
$this->assertEquals('You have 2 unseeded snatches to save', $pm->subject(), 'unseeded-initial-1-rescue');
$this->assertStringContainsString("In the past, you snatched 2 uploads that are no longer being seeded", $body, 'unseeded-initial-1-body');
$this->assertStringContainsString("[pl]{$this->torrentList[0]->id()}[/pl]", $body, 'unseeded-initial-1-body-pl-0');
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'unseeded-initial-1-body-pl-1');
$pm->remove();
$initialClaim = $reaper->claimStats();
$this->assertEquals(['open', 'claimed'], array_keys($initialClaim), 'claim-initial');
$this->assertEquals(2, $initialClaim['open'], 'claim-open');
$this->assertEquals(0, $initialClaim['claimed'], 'claim-claimed');
// snatcher reseeds the first upload
$this->modifyUnseededInterval($this->torrentList[0], NOTIFY_UNSEEDED_INITIAL_HOUR + 3);
$this->generateReseed($this->torrentList[0], $this->userList[1]);
// and wins the glory
$bonus = $this->userList[1]->bonusPointsTotal();
$win = $reaper->claim();
$this->assertCount(1, $win, 'unseeded-claim-win-count');
[$torrentId, $userId, $bp] = $win[0];
$this->assertEquals($this->torrentList[0]->id(), $torrentId, 'unseeded-claim-win-torrent');
$this->assertEquals($this->userList[1]->id(), $userId, 'unseeded-claim-win-user');
$this->assertEquals($this->userList[1]->flush()->bonusPointsTotal(), $bonus + $bp, 'unseeded-claim-win-bp');
// message
$pm = $inboxList[1]->messageList($pmMan[1], 1, 0)[0];
$body = $pm->postList(1, 0)[0]['body'];
$this->assertEquals("Thank you for reseeding {$this->torrentList[0]->group()->name()}!", $pm->subject(), 'unseeded-initial-1-thx');
$this->assertStringContainsString("[pl]{$this->torrentList[0]->id()}[/pl]", $body, 'unseeded-initial-1-body-pl-thx');
$this->assertStringContainsString("$bp bonus points", $body, 'unseeded-initial-1-body-bp-thx');
$pm->remove();
$newClaim = $reaper->claimStats();
$this->assertEquals(1, $newClaim['open'], 'claim-new-open');
$this->assertEquals(1, $newClaim['claimed'], 'claim-new-claimed');
// reset the time of the remaing unseeded alert back in time to hit the final timeout.
$this->modifyLastAction($this->torrentList[1], NOTIFY_UNSEEDED_FINAL_HOUR + 2);
$this->modifyUnseededInterval($this->torrentList[1], NOTIFY_UNSEEDED_FINAL_HOUR + 1);
$unseededStats = $reaper->stats();
$unseededFinal = $reaper->finalUnseededList();
$this->assertCount(1, $unseededFinal, 'unseeded-final-0');
$this->assertEquals(1,
$reaper->process(
$unseededFinal,
Gazelle\Torrent\ReaperState::UNSEEDED,
Gazelle\Torrent\ReaperNotify::FINAL
),
'unseeded-final-process'
);
$this->assertEquals(
[
"never_seeded_initial" => 0,
"never_seeded_final" => 0,
"unseeded_initial" => 0,
"unseeded_final" => $unseededStats['unseeded_final'] + 1,
],
$reaper->stats(),
'reaper-unseeded-stats-final'
);
$this->assertEquals(1, $inboxList[0]->messageTotal(), 'unseeded-final-0-inbox');
$pm = $inboxList[0]->messageList($pmMan[0], 1, 0)[0];
$body = $pm->postList(1, 0)[0]['body'];
$this->assertEquals('There is an unseeded upload scheduled for deletion very soon', $pm->subject(), 'unseeded-final-0-rescue');
$this->assertStringContainsString("You have an upload that is still not currently seeding by you (or anyone else)", $body, 'unseeded-final-0-body');
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'unseeded-final-0-body-pl-1');
$pm->remove();
$this->assertEquals(0, $inboxList[1]->messageTotal(), 'unseeded-final-no-snatcher-inbox');
// too late
$this->modifyUnseededInterval($this->torrentList[1], REMOVE_UNSEEDED_HOUR + 1);
$id = $this->torrentList[1]->id();
$list = $reaper->reaperList(
state: Gazelle\Torrent\ReaperState::UNSEEDED,
interval: REMOVE_UNSEEDED_HOUR,
);
$this->assertCount(1, $list, 'unseeded-reap-list');
$this->assertEquals(1, $reaper->removeUnseeded(), 'unseeded-reap-remove');
$pm = $inboxList[0]->messageList($pmMan[0], 1, 0)[0];
$body = $pm->postList(1, 0)[0]['body'];
$this->assertEquals('1 of your uploads has been deleted for inactivity (unseeded)', $pm->subject(), 'never-remove-message');
$this->assertStringContainsString("Dear {$this->userList[0]->username()}", $body, 'never-remove-body-dear');
$this->assertStringContainsString("[url=torrents.php?id={$this->torrentList[1]->group()->id()}]", $body, 'never-remove-body-pl');
$pm->remove();
$pm = $inboxList[1]->messageList($pmMan[1], 1, 0)[0];
$body = $pm->postList(1, 0)[0]['body'];
$this->assertEquals('1 of your snatches was deleted for inactivity', $pm->subject(), 'never-remove-snatcher-message');
$this->assertStringContainsString("Dear {$this->userList[1]->username()}", $body, 'never-remove-snatcher-body-dear');
$this->assertStringContainsString("[url=torrents.php?id={$this->torrentList[1]->group()->id()}]", $body, 'never-remove-snatcher-body-pl');
$pm->remove();
$deleted = $torMan->findById($id);
$this->assertNull($deleted, 'unseeded-was-reaped');
}
public function testNeverNotify(): void {
// like never, but checking that people who have asked not to receive notifications, do not.
$this->userList[0]->toggleAttr('no-pm-unseeded-upload', true);
$this->userList[1]->toggleAttr('no-pm-unseeded-snatch', true);
$this->assertTrue($this->userList[0]->hasAttr('no-pm-unseeded-upload'), 'reaper-no-notify-upload');
$this->assertTrue($this->userList[1]->hasAttr('no-pm-unseeded-snatch'), 'reaper-no-notify-snatch');
// reset the last action and time of the unseeded alert back in time to hit the initial timeout
foreach ($this->torrentList as $torrent) {
$hour = NOTIFY_NEVER_SEEDED_INITIAL_HOUR + 1;
$torrent->setField('created', date('Y-m-d H:i:s', strtotime("-{$hour} hours")))->modify();
$this->modifyLastAction($torrent, NOTIFY_NEVER_SEEDED_INITIAL_HOUR + 2);
}
// look for never seeded
$reaper = new \Gazelle\Torrent\Reaper(new Gazelle\Manager\Torrent, new Gazelle\Manager\User);
$reaper->process(
$reaper->initialNeverSeededList(),
Gazelle\Torrent\ReaperState::NEVER,
Gazelle\Torrent\ReaperNotify::INITIAL
);
$this->assertEquals(0, (new Gazelle\User\Inbox($this->userList[0]))->messageTotal(), 'never-uploader-no-notify');
$this->assertEquals(0, (new Gazelle\User\Inbox($this->userList[1]))->messageTotal(), 'never-snatcher-no-notify');
}
public function testUnseededNotify(): void {
// like unseeded, but checking that people who have asked not to receive notifications, do not.
$this->userList[0]->toggleAttr('no-pm-unseeded-upload', true);
$this->userList[1]->toggleAttr('no-pm-unseeded-snatch', true);
$this->assertTrue($this->userList[0]->hasAttr('no-pm-unseeded-upload'), 'reaper-no-notify-upload');
$this->assertTrue($this->userList[1]->hasAttr('no-pm-unseeded-snatch'), 'reaper-no-notify-snatch');
// reset the last action and time of the unseeded alert back in time to hit the initial timeout
foreach ($this->torrentList as $torrent) {
$hour = NOTIFY_UNSEEDED_INITIAL_HOUR + 1;
$torrent->setField('created', date('Y-m-d H:i:s', strtotime("-{$hour} hours")))->modify();
$this->modifyLastAction($torrent, NOTIFY_UNSEEDED_INITIAL_HOUR + 2);
$this->generateSnatch($torrent, $this->userList[1]);
}
// look for unseeded
$reaper = new \Gazelle\Torrent\Reaper(new Gazelle\Manager\Torrent, new Gazelle\Manager\User);
$reaper->process(
$reaper->initialUnseededList(),
Gazelle\Torrent\ReaperState::UNSEEDED,
Gazelle\Torrent\ReaperNotify::INITIAL
);
$this->assertEquals(1, (new Gazelle\User\Inbox($this->userList[0]))->messageTotal(), 'unseeded-uploader-no-notify');
$this->assertEquals(0, (new Gazelle\User\Inbox($this->userList[1]))->messageTotal(), 'unseeded-snatcher-no-notify');
$timeline = $reaper->timeline();
$this->assertEquals([2], array_values($timeline), 'reaper-two-today');
$today = current(array_keys($timeline));
// take into account the chance of running a few seconds before midnight
$this->assertContains($today, [date('Y-m-d'), date('Y-m-d', strtotime('1 hour ago'))], 'reaper-timeline');
}
public function testGracePeriod(): void {
// Check that the unseeded_date can be extended
foreach ($this->torrentList as $torrent) {
$hour = NOTIFY_UNSEEDED_INITIAL_HOUR + 1;
$torrent->setField('created', date('Y-m-d H:i:s', strtotime("-{$hour} hours")))->modify();
$this->modifyLastAction($torrent, NOTIFY_UNSEEDED_INITIAL_HOUR + 2);
}
$reaper = new \Gazelle\Torrent\Reaper(new Gazelle\Manager\Torrent, new Gazelle\Manager\User);
$reaper->process(
$reaper->initialUnseededList(),
Gazelle\Torrent\ReaperState::UNSEEDED,
Gazelle\Torrent\ReaperNotify::INITIAL
);
$uploaderId = $this->userList[0]->id();
$initial = array_filter(
$reaper->unseederList(),
fn($user) => $user['user_id'] == $uploaderId
);
$this->assertCount(1, $initial, 'reaper-grace-period-initial');
$this->assertEquals(
2, // two torrents will be extended 10 days
$reaper->extendGracePeriod([$uploaderId], 10),
'reaper-grace-period-extend'
);
$final = array_filter(
$reaper->unseederList(),
fn($user) => $user['user_id'] == $uploaderId
);
$this->assertEquals(
864000,
strtotime(array_values($final)[0]['min_date']) - strtotime(array_values($initial)[0]['min_date']),
'reaper-grace-period-delta'
);
}
}