Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions apps/files_versions/tests/GetAutoExpireListTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Versions\Tests;

use OCA\Files_Versions\Storage;
use Test\TestCase;

class GetAutoExpireListTest extends TestCase {

/**
* Frozen reference time for all tests
*/
private const NOW = 1600000000;

/**
* Helper to call the private retention logic
*
* @param int $now
* @param array $versions
* @return array{array<int,array>, int}
*/
private static function callGetAutoExpireList(int $now, array $versions): array {
$ref = new \ReflectionClass(Storage::class);
$method = $ref->getMethod('getAutoExpireList');

/** @var array{array<int,array>, int} */
return $method->invoke(null, $now, $versions);
}

#[\PHPUnit\Framework\Attributes\DataProvider('provideBucketKeepsLatest')]
public function testBucketKeepsLatest(int $age1, int $age2, int $size1, int $size2): void {
$now = time();

$first = $now - $age1;
$second = $now - $age2;

// Ensure first is newer than second
if ($first < $second) {
[$first, $second] = [$second, $first];
[$size1, $size2] = [$size2, $size1];
}

$versions = [
$first => ['version' => $first, 'size' => $size1, 'path' => 'f'],
$second => ['version' => $second, 'size' => $size2, 'path' => 'f'],
];

[$toDelete, $size] = self::callGetAutoExpireList($now, $versions);

$deletedKeys = array_map('intval', array_keys($toDelete));

$this->assertEquals([$second], $deletedKeys, 'Older version was not deleted');
$this->assertEquals($versions[$second]['size'], $size, 'Deleted size mismatch');
}

public static function provideBucketKeepsLatest(): array {
$DAY = 24 * 60 * 60;

return [
'minute' => [
8, // 8s old
9, // 9s old
5,
6,
],
'hour' => [
120, // 2 minutes old
150, // 2m30s old
10,
11,
],
'day' => [
5 * 3600, // 5 hours old
5 * 3600 + 1800, // 5.5h old
20,
21,
],
'week' => [
2 * $DAY, // 2 days old
2 * $DAY + 6 * 3600, // 2.25 days old
40,
41,
],
'month' => [
5 * $DAY, // 5 days old
5 * $DAY + 12 * 3600, // 5.5 days old
30,
31,
],
'year' => [
35 * $DAY, // 35 days old
37 * $DAY, // 37 days old
42,
43,
],
'beyond-year' => [
400 * $DAY, // ~13.3 months old
405 * $DAY, // ~13.5 months old
50,
51,
],
];
}

#[\PHPUnit\Framework\Attributes\DataProvider('provideVersionRetentionRanges')]
public function testRetentionOverTimeEveryTenMinutes(
int $days,
int $expectedMin,
int $expectedMax,
): void {
$now = time();
$versions = [];

// One version every 10 minutes
$interval = 600; // 10 minutes
$total = $days * 24 * 6;

for ($i = 0; $i < $total; $i++) {
$ts = $now - ($i * $interval);
$versions[$ts] = [
'version' => $ts,
'size' => 1,
'path' => 'f',
];
}

[$toDelete, $size] = self::callGetAutoExpireList($now, $versions);

$retained = array_diff(array_keys($versions), array_keys($toDelete));
$retainedCount = count($retained);

$this->assertGreaterThanOrEqual(
$expectedMin,
$retainedCount,
"Too few versions retained for {$days} days"
);

$this->assertLessThanOrEqual(
$expectedMax,
$retainedCount,
"Too many versions retained for {$days} days"
);
}

public static function provideVersionRetentionRanges(): array {
return [
'5 days' => [
5,
28,
33,
],
'30 days' => [
30,
54,
60,
],
'1 year' => [
365,
100,
140,
],
];
}

#[\PHPUnit\Framework\Attributes\DataProvider('provideExactRetentionCounts')]
public function testExactRetentionCounts(
int $days,
int $expectedRetained,
): void {
$now = self::NOW;
$versions = [];

// One version per hour, safely inside bucket slots
for ($i = 0; $i < $days * 24; $i++) {
$ts = $now - ($i * 3600) - 1;
$versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f'];
}

[$toDelete] = self::callGetAutoExpireList($now, $versions);
$retained = array_diff_key($versions, $toDelete);

$this->assertSame(
$expectedRetained,
count($retained),
"Exact retention count mismatch for {$days} days"
);
}

/**
* @return array<string, array{int,int}>
*/
public static function provideExactRetentionCounts(): array {
return [
'five-days' => [
5,
self::expectedHourlyRetention(5),
],
'thirty-days' => [
30,
self::expectedHourlyRetention(30),
],
'one-year' => [
365,
self::expectedHourlyRetention(365),
],
'one-year-plus' => [
500,
self::expectedHourlyRetention(500),
],
];
}

private static function expectedHourlyRetention(int $days): int {
// Hourly for first day
$hourly = min(24, $days * 24);

// Daily from day 1 to day 30
$dailyDays = max(0, min($days, 30) - 1);
$daily = $dailyDays;

// Weekly beyond 30 days
$weeklyDays = max(0, $days - 30);
$weekly = intdiv($weeklyDays, 7) + ($weeklyDays > 0 ? 1 : 0);

return $hourly + $daily + $weekly;
}
}
Loading