Skip to content
Open
Changes from 1 commit
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
226 changes: 226 additions & 0 deletions apps/files_versions/tests/GetAutoExpireListTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2025 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files_Versions\Tests;

use OCA\Files_Versions\Storage;
use ReflectionClass;
use ReflectionException;

class GetAutoExpireListTest extends \Test\TestCase {

/**
* @throws ReflectionException
* @since 32.0.0
*/
protected static function callGetAutoExpireList(int $time, array $versions): array {
$ref = new ReflectionClass(Storage::class);
$method = $ref->getMethod('getAutoExpireList');
$method->setAccessible(true);

return $method->invokeArgs(null, [$time, $versions]);
}

/**
* @since 32.0.0
* @dataProvider provideBucketKeepsLatest
*/
public function testBucketKeepsLatest(int $offset1, int $offset2, int $size1, int $size2) {
$now = time();

$first = $now - $offset1;
$second = $first - $offset2;

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

/**
* Provides test cases for different bucket intervals.
* Each case is [offset1 (age of first), offset2 (extra gap for second), size1, size2].
* @return array<string, array{int,int,int,int}>
*/
public static function provideBucketKeepsLatest(): array {
$DAY = 24 * 60 * 60;
$WEEK = 7 * $DAY;

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

/**
* @since 32.0.0
*/
public function testFiveDaysOfVersionsEveryTenMinutes() {
$now = time();
$versions = [];

// Create one version every 10 minutes for 5 days
for ($i = 0; $i < (5 * 24 * 6); $i++) {
$ts = $now - ($i * 600);
$versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f'];
}

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

// Expect ~28-33 retained due to bucket rules
$this->assertGreaterThanOrEqual(28, count($retained));
$this->assertLessThanOrEqual(33, count($retained));
}

/**
* @since 32.0.0
*/
public function testThirtyDaysOfVersionsEveryTenMinutes() {
$now = time();
$versions = [];

// Create one version every 10 minutes for 30 days
for ($i = 0; $i < (30 * 24 * 6); $i++) {
$ts = $now - ($i * 600);
$versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f'];
}

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

// Expect ~54-60 retained (24 hours hourly + 29 daily + bucket overlap)
$this->assertGreaterThanOrEqual(54, count($retained));
$this->assertLessThanOrEqual(60, count($retained));
}

/**
* @since 32.0.0
*/
public function testYearOfVersionsEveryTenMinutes() {
$now = time();
$versions = [];

// Create one version every 10 minutes for 365 days
for ($i = 0; $i < (365 * 24 * 6); $i++) {
$ts = $now - ($i * 600);
$versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f'];
}

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

// Expect ~100-140 retained due to buckets (minute, hour, day, week, month)
$this->assertGreaterThanOrEqual(100, count($retained));
$this->assertLessThanOrEqual(140, count($retained));
}

/**
* @since 32.0.0
*/
public function testMoreThanAYearOfVersionsEveryTenMinutesWithDeletion() {
$now = time();
$versions = [];

// Define bucket steps (same as retention logic)
$buckets = [
1 => ['intervalEndsAfter' => 10, 'step' => 2],
2 => ['intervalEndsAfter' => 60, 'step' => 10],
3 => ['intervalEndsAfter' => 3600, 'step' => 60],
4 => ['intervalEndsAfter' => 86400, 'step' => 3600],
5 => ['intervalEndsAfter' => 2592000, 'step' => 86400],
6 => ['intervalEndsAfter' => -1, 'step' => 604800],
];

$lastBoundary = 0;
foreach ($buckets as $bucket) {
$intervalEnd = $bucket['intervalEndsAfter'] > 0 ? $bucket['intervalEndsAfter'] : 500 * 86400;
$step = $bucket['step'];

for ($age = $lastBoundary; $age <= $intervalEnd; $age += $step) {
// Add multiple versions per step (3 versions spaced evenly within step)
for ($i = 0; $i < 3; $i++) {
$ts = $now - ($age + $i * floor($step / 3));
$versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f'];
}
}

$lastBoundary = $intervalEnd;
}

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

$lastBoundary = 0;
foreach ($buckets as $bucket) {
$intervalEnd = $bucket['intervalEndsAfter'] > 0 ? $bucket['intervalEndsAfter'] : PHP_INT_MAX;

$bucketRetained = array_filter($retained, function ($ts) use ($now, $lastBoundary, $intervalEnd) {
$age = $now - $ts;
return $age >= $lastBoundary && $age <= $intervalEnd;
});

$this->assertGreaterThanOrEqual(
1,
count($bucketRetained),
"Bucket ending at $intervalEnd seconds has " . count($bucketRetained) . ' retained, expected at least 1'
);

$lastBoundary = $intervalEnd;
}

}

}
Loading