|
| 1 | +<?php |
| 2 | + |
| 3 | +/** |
| 4 | + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
| 5 | + * SPDX-FileCopyrightText: 2025 ownCloud, Inc. |
| 6 | + * SPDX-License-Identifier: AGPL-3.0-only |
| 7 | + */ |
| 8 | +namespace OCA\Files_Versions\Tests; |
| 9 | + |
| 10 | +use OCA\Files_Versions\Storage; |
| 11 | +use ReflectionClass; |
| 12 | +use ReflectionException; |
| 13 | + |
| 14 | +class GetAutoExpireListTest extends \Test\TestCase { |
| 15 | + |
| 16 | + /** |
| 17 | + * @throws ReflectionException |
| 18 | + * @since 33.0.0 |
| 19 | + */ |
| 20 | + protected static function callGetAutoExpireList(int $time, array $versions): array { |
| 21 | + $ref = new ReflectionClass(Storage::class); |
| 22 | + $method = $ref->getMethod('getAutoExpireList'); |
| 23 | + $method->setAccessible(true); |
| 24 | + |
| 25 | + return $method->invokeArgs(null, [$time, $versions]); |
| 26 | + } |
| 27 | + |
| 28 | + /** |
| 29 | + * @since 33.0.0 |
| 30 | + * @dataProvider provideBucketKeepsLatest |
| 31 | + */ |
| 32 | + public function testBucketKeepsLatest(int $offset1, int $offset2, int $size1, int $size2) { |
| 33 | + $now = time(); |
| 34 | + |
| 35 | + $first = $now - $offset1; |
| 36 | + $second = $first - $offset2; |
| 37 | + |
| 38 | + $versions = [ |
| 39 | + $first => ['version' => $first, 'size' => $size1, 'path' => 'f'], |
| 40 | + $second => ['version' => $second, 'size' => $size2, 'path' => 'f'], |
| 41 | + ]; |
| 42 | + |
| 43 | + [$toDelete, $size] = self::callGetAutoExpireList($now, $versions); |
| 44 | + |
| 45 | + $deletedKeys = array_map('intval', array_keys($toDelete)); |
| 46 | + |
| 47 | + $this->assertEquals([$second], $deletedKeys, "Older version was not deleted"); |
| 48 | + $this->assertEquals($versions[$second]['size'], $size, "Deleted size mismatch"); |
| 49 | + } |
| 50 | + |
| 51 | + /** |
| 52 | + * Provides test cases for different bucket intervals. |
| 53 | + * Each case is [offset1 (age of first), offset2 (extra gap for second), size1, size2]. |
| 54 | + * @return array<string, array{int,int,int,int}> |
| 55 | + */ |
| 56 | + public static function provideBucketKeepsLatest(): array { |
| 57 | + $DAY = 24 * 60 * 60; |
| 58 | + $WEEK = 7 * $DAY; |
| 59 | + |
| 60 | + return [ |
| 61 | + 'minute' => [ |
| 62 | + 8, // 8s old |
| 63 | + 1, // 9s old → both in same 2s slot |
| 64 | + 5, |
| 65 | + 6, |
| 66 | + ], |
| 67 | + 'hour' => [ |
| 68 | + 2 * 60, // 2 minutes old |
| 69 | + 30, // 2m30s old → both in same 1m slot |
| 70 | + 10, |
| 71 | + 11, |
| 72 | + ], |
| 73 | + 'day' => [ |
| 74 | + 5 * 3600, // 5 hours old |
| 75 | + 1800, // 5.5h old → both in same 1h slot |
| 76 | + 20, |
| 77 | + 21, |
| 78 | + ], |
| 79 | + 'week' => [ |
| 80 | + 2 * $DAY, // 2 days old |
| 81 | + 6 * 3600, // 2.25 days old → both in same 1d slot |
| 82 | + 40, |
| 83 | + 41, |
| 84 | + ], |
| 85 | + 'month' => [ |
| 86 | + 5 * $DAY, // 5 days old |
| 87 | + 12 * 60 * 60, // 5.5 days old → both in same 1d slot |
| 88 | + 30, |
| 89 | + 31, |
| 90 | + ], |
| 91 | + 'year' => [ |
| 92 | + 35 * $DAY, // 35 days old |
| 93 | + 2 * $DAY, // 37 days old → both in same 1w slot |
| 94 | + 42, |
| 95 | + 43, |
| 96 | + ], |
| 97 | + 'beyond-year' => [ |
| 98 | + 400 * $DAY, // ~13.3 months old |
| 99 | + 5 * $DAY, // 405 days old → same 30d slot |
| 100 | + 50, |
| 101 | + 51, |
| 102 | + ], |
| 103 | + ]; |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * @since 33.0.0 |
| 108 | + */ |
| 109 | + public function testFiveDaysOfVersionsEveryTenMinutes() { |
| 110 | + $now = time(); |
| 111 | + $versions = []; |
| 112 | + |
| 113 | + // Create one version every 10 minutes for 5 days |
| 114 | + for ($i = 0; $i < (5 * 24 * 6); $i++) { |
| 115 | + $ts = $now - ($i * 600); |
| 116 | + $versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f']; |
| 117 | + } |
| 118 | + |
| 119 | + [$toDelete, $size] = self::callGetAutoExpireList($now, $versions); |
| 120 | + $retained = array_diff(array_keys($versions), array_keys($toDelete)); |
| 121 | + |
| 122 | + // Expect ~28-33 retained due to bucket rules |
| 123 | + $this->assertGreaterThanOrEqual(28, count($retained)); |
| 124 | + $this->assertLessThanOrEqual(33, count($retained)); |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * @since 33.0.0 |
| 129 | + */ |
| 130 | + public function testThirtyDaysOfVersionsEveryTenMinutes() { |
| 131 | + $now = time(); |
| 132 | + $versions = []; |
| 133 | + |
| 134 | + // Create one version every 10 minutes for 30 days |
| 135 | + for ($i = 0; $i < (30 * 24 * 6); $i++) { |
| 136 | + $ts = $now - ($i * 600); |
| 137 | + $versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f']; |
| 138 | + } |
| 139 | + |
| 140 | + [$toDelete, $size] = self::callGetAutoExpireList($now, $versions); |
| 141 | + $retained = array_diff(array_keys($versions), array_keys($toDelete)); |
| 142 | + |
| 143 | + // Expect ~54-60 retained (24 hours hourly + 29 daily + bucket overlap) |
| 144 | + $this->assertGreaterThanOrEqual(54, count($retained)); |
| 145 | + $this->assertLessThanOrEqual(60, count($retained)); |
| 146 | + } |
| 147 | + |
| 148 | + /** |
| 149 | + * @since 33.0.0 |
| 150 | + */ |
| 151 | + public function testYearOfVersionsEveryTenMinutes() { |
| 152 | + $now = time(); |
| 153 | + $versions = []; |
| 154 | + |
| 155 | + // Create one version every 10 minutes for 365 days |
| 156 | + for ($i = 0; $i < (365 * 24 * 6); $i++) { |
| 157 | + $ts = $now - ($i * 600); |
| 158 | + $versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f']; |
| 159 | + } |
| 160 | + |
| 161 | + [$toDelete, $size] = self::callGetAutoExpireList($now, $versions); |
| 162 | + $retained = array_diff(array_keys($versions), array_keys($toDelete)); |
| 163 | + |
| 164 | + // Expect ~100-140 retained due to buckets (minute, hour, day, week, month) |
| 165 | + $this->assertGreaterThanOrEqual(100, count($retained)); |
| 166 | + $this->assertLessThanOrEqual(140, count($retained)); |
| 167 | + } |
| 168 | + |
| 169 | + /** |
| 170 | + * @since 33.0.0 |
| 171 | + */ |
| 172 | + public function testMoreThanAYearOfVersionsEveryTenMinutesWithDeletion() { |
| 173 | + $now = time(); |
| 174 | + $versions = []; |
| 175 | + |
| 176 | + // Define bucket steps (same as retention logic) |
| 177 | + $buckets = [ |
| 178 | + 1 => ['intervalEndsAfter' => 10, 'step' => 2], |
| 179 | + 2 => ['intervalEndsAfter' => 60, 'step' => 10], |
| 180 | + 3 => ['intervalEndsAfter' => 3600, 'step' => 60], |
| 181 | + 4 => ['intervalEndsAfter' => 86400, 'step' => 3600], |
| 182 | + 5 => ['intervalEndsAfter' => 2592000, 'step' => 86400], |
| 183 | + 6 => ['intervalEndsAfter' => -1, 'step' => 604800], |
| 184 | + ]; |
| 185 | + |
| 186 | + $lastBoundary = 0; |
| 187 | + foreach ($buckets as $bucket) { |
| 188 | + $intervalEnd = $bucket['intervalEndsAfter'] > 0 ? $bucket['intervalEndsAfter'] : 500 * 86400; |
| 189 | + $step = $bucket['step']; |
| 190 | + |
| 191 | + for ($age = $lastBoundary; $age <= $intervalEnd; $age += $step) { |
| 192 | + // Add multiple versions per step (3 versions spaced evenly within step) |
| 193 | + for ($i = 0; $i < 3; $i++) { |
| 194 | + $ts = $now - ($age + $i * floor($step / 3)); |
| 195 | + $versions[$ts] = ['version' => $ts, 'size' => 1, 'path' => 'f']; |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + $lastBoundary = $intervalEnd; |
| 200 | + } |
| 201 | + |
| 202 | + [$toDelete, $size] = self::callGetAutoExpireList($now, $versions); |
| 203 | + $retained = array_diff(array_keys($versions), array_keys($toDelete)); |
| 204 | + |
| 205 | + $lastBoundary = 0; |
| 206 | + foreach ($buckets as $bucket) { |
| 207 | + $intervalEnd = $bucket['intervalEndsAfter'] > 0 ? $bucket['intervalEndsAfter'] : PHP_INT_MAX; |
| 208 | + |
| 209 | + $bucketRetained = array_filter($retained, function ($ts) use ($now, $lastBoundary, $intervalEnd) { |
| 210 | + $age = $now - $ts; |
| 211 | + return $age >= $lastBoundary && $age <= $intervalEnd; |
| 212 | + }); |
| 213 | + |
| 214 | + $this->assertGreaterThanOrEqual( |
| 215 | + 1, |
| 216 | + count($bucketRetained), |
| 217 | + "Bucket ending at $intervalEnd seconds has " . count($bucketRetained) . " retained, expected at least 1" |
| 218 | + ); |
| 219 | + |
| 220 | + $lastBoundary = $intervalEnd; |
| 221 | + } |
| 222 | + |
| 223 | + } |
| 224 | + |
| 225 | +} |
0 commit comments