diff --git a/.github/workflows/run-tests-l11.yml b/.github/workflows/run-tests-l11.yml index 367f943..119319e 100644 --- a/.github/workflows/run-tests-l11.yml +++ b/.github/workflows/run-tests-l11.yml @@ -11,6 +11,18 @@ jobs: tests: runs-on: ubuntu-latest + + services: + mongodb: + image: mongo:7 + ports: + - 27017:27017 + options: >- + --health-cmd="mongosh --quiet --eval 'db.runCommand({ping:1})'" + --health-interval 10s + --health-timeout 5s + --health-retries 3 + strategy: fail-fast: false matrix: @@ -18,22 +30,39 @@ jobs: laravel: [ 11.* ] include: - laravel: 11.* - testbench: 8.* + testbench: 9.* name: P${{ matrix.php }} - L${{ matrix.laravel }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: pdo, sqlite, pdo_sqlite + extensions: pdo, sqlite, pdo_sqlite, mongodb + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- - name: Install Dependencies - run: composer install + run: composer install --prefer-dist --no-interaction --no-progress + - name: Execute tests run: vendor/bin/phpunit + env: + MONGODB_HOST: 127.0.0.1 + MONGODB_PORT: 27017 + MONGODB_DATABASE: laravel_mongodb_cache_test diff --git a/.gitignore b/.gitignore index 0794651..c2d7942 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /vendor/ composer.lock -.phpunit.result.cache +.phpunit.cache diff --git a/.phprc b/.phprc index 6e3833a..a5a984f 100644 --- a/.phprc +++ b/.phprc @@ -1 +1 @@ -php8.1 +php8.2 diff --git a/README.md b/README.md index 13015cf..c5f0f9e 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,59 @@ Advantages php artisan mongodb:cache:dropindex +Testing +------- + +This package includes tests that interact with a real MongoDB database to verify the functionality of the cache driver. The tests require a MongoDB instance to run successfully. + +To run the tests: + +1. Make sure you have MongoDB installed and running on your local machine +2. The test configuration is set in `phpunit.xml`: + +```xml + + + + + + + +``` + +3. Run the tests with: + +``` +composer test +``` + +or + +``` +vendor/bin/phpunit +``` + +### Test Structure + +The test suite is organized into multiple files to test various aspects of the MongoDB cache driver: + +- `StoreTest.php`: Tests basic Store class functionality (get, put, forget, flush) +- `AdvancedCacheFeaturesTest.php`: Tests advanced Store features like increment/decrement, forever storage, and handling arrays/objects +- `TaggedCacheTest.php`: Tests tagged cache functionality +- `LaravelIntegrationTest.php`: Tests integration with Laravel's Cache facade + +Some functionality (like increment/decrement, forever storage) is intentionally tested in multiple contexts: +1. At the low-level Store implementation +2. Through Laravel's Cache facade +3. With tagged cache operations + +This multi-layered approach ensures that all feature functionality works correctly at all levels of integration. + +GitHub Actions +------------- + +The package includes GitHub Actions workflows that automatically run tests against a MongoDB service. The MongoDB service is started as part of the CI workflow, ensuring tests are executed in an environment with a real MongoDB database. + Warning ------- diff --git a/composer.json b/composer.json index c1768ad..9c9a644 100644 --- a/composer.json +++ b/composer.json @@ -2,11 +2,11 @@ "name": "1ff/laravel-mongodb-cache", "description": "A mongodb cache driver for laravel", "type": "library", - "version": "7.0.0", "require": { "php": "^8.2|^8.3", "illuminate/cache": "^11.0", - "mongodb/laravel-mongodb": "^4.1" + "mongodb/laravel-mongodb": "^5.0", + "ext-mongodb": "*" }, "require-dev": { "orchestra/testbench": "^9.0" @@ -39,5 +39,8 @@ "ForFit\\Mongodb\\Cache\\ServiceProvider" ] } + }, + "scripts": { + "test": "vendor/bin/phpunit" } } diff --git a/phpunit.xml b/phpunit.xml index 5733f58..b374467 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,17 +1,8 @@ - + tests @@ -19,5 +10,10 @@ + + + + + diff --git a/src/Console/Commands/MongodbCacheDropIndex.php b/src/Console/Commands/MongodbCacheDropIndex.php index 5db9359..b581c89 100644 --- a/src/Console/Commands/MongodbCacheDropIndex.php +++ b/src/Console/Commands/MongodbCacheDropIndex.php @@ -4,7 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; -use \MongoDB\Driver\ReadPreference; +use MongoDB\Driver\ReadPreference; /** * Drop the indexes created by MongodbCacheIndex @@ -12,34 +12,22 @@ class MongodbCacheDropIndex extends Command { - /** - * The name and signature of the console command. - * - * @var string - */ + /** The name and signature of the console command. */ protected $signature = 'mongodb:cache:dropindex {index}'; - /** - * The console command description. - * - * @var string - */ + /** The console command description. */ protected $description = 'Drops the passed index from the mongodb `cache` collection'; - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() + /** Execute the console command. */ + public function handle(): void { $cacheCollectionName = config('cache')['stores']['mongodb']['table']; - DB::connection('mongodb')->getMongoDB()->command([ + DB::connection('mongodb')->getDatabase()->command([ 'dropIndexes' => $cacheCollectionName, 'index' => $this->argument('index'), ], [ - 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY) + 'readPreference' => new ReadPreference(ReadPreference::PRIMARY) ]); } } diff --git a/src/Console/Commands/MongodbCacheIndex.php b/src/Console/Commands/MongodbCacheIndex.php index a2f7b43..7dfb698 100644 --- a/src/Console/Commands/MongodbCacheIndex.php +++ b/src/Console/Commands/MongodbCacheIndex.php @@ -4,7 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; -use \MongoDB\Driver\ReadPreference; +use MongoDB\Driver\ReadPreference; /** * Create indexes for the cache collection @@ -12,30 +12,18 @@ class MongodbCacheIndex extends Command { - /** - * The name and signature of the console command. - * - * @var string - */ + /** The name and signature of the console command. */ protected $signature = 'mongodb:cache:index'; - /** - * The console command description. - * - * @var string - */ + /** The console command description. */ protected $description = 'Create indexes on the mongodb `cache` collection'; - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() + /** Execute the console command. */ + public function handle(): void { $cacheCollectionName = config('cache')['stores']['mongodb']['table']; - DB::connection('mongodb')->getMongoDB()->command([ + DB::connection('mongodb')->getDatabase()->command([ 'createIndexes' => $cacheCollectionName, 'indexes' => [ [ @@ -52,7 +40,7 @@ public function handle() ] ] ], [ - 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY) + 'readPreference' => new ReadPreference(ReadPreference::PRIMARY) ]); } } diff --git a/src/Console/Commands/MongodbCacheIndexTags.php b/src/Console/Commands/MongodbCacheIndexTags.php index 1a2de24..96ebe28 100644 --- a/src/Console/Commands/MongodbCacheIndexTags.php +++ b/src/Console/Commands/MongodbCacheIndexTags.php @@ -4,7 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; -use \MongoDB\Driver\ReadPreference; +use MongoDB\Driver\ReadPreference; /** * Create indexes for the cache collection @@ -12,30 +12,18 @@ class MongodbCacheIndexTags extends Command { - /** - * The name and signature of the console command. - * - * @var string - */ + /** The name and signature of the console command. */ protected $signature = 'mongodb:cache:index_tags'; - /** - * The console command description. - * - * @var string - */ + /** The console command description. */ protected $description = 'Create indexes on the tags column of mongodb `cache` collection'; - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() + /** Execute the console command.*/ + public function handle(): void { $cacheCollectionName = config('cache')['stores']['mongodb']['table']; - DB::connection('mongodb')->getMongoDB()->command([ + DB::connection('mongodb')->getDatabase()->command([ 'createIndexes' => $cacheCollectionName, 'indexes' => [ [ @@ -45,7 +33,7 @@ public function handle() ], ] ], [ - 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY) + 'readPreference' => new ReadPreference(ReadPreference::PRIMARY) ]); } } diff --git a/src/MongoTaggedCache.php b/src/MongoTaggedCache.php index 7c1d048..bfa0644 100644 --- a/src/MongoTaggedCache.php +++ b/src/MongoTaggedCache.php @@ -2,16 +2,16 @@ namespace ForFit\Mongodb\Cache; +use Illuminate\Cache\Events\KeyWritten; use Illuminate\Cache\Repository; use Illuminate\Contracts\Cache\Store; -use Illuminate\Cache\Events\KeyWritten; class MongoTaggedCache extends Repository { - protected $tags; + protected array $tags; /** - * @param \Illuminate\Contracts\Cache\Store $store + * @param Store $store * @param array $tags */ public function __construct(Store $store, array $tags = []) @@ -27,12 +27,12 @@ public function __construct(Store $store, array $tags = []) * @param string $key * @param mixed $value * @param \DateTimeInterface|\DateInterval|float|int $ttl - * @return void + * @return bool|null */ public function put($key, $value, $ttl = null) { if (is_array($key)) { - return $this->putMany($key, $value); + $this->putMany($key, $value); } $seconds = $this->getSeconds(is_null($ttl) ? 315360000 : $ttl); @@ -45,19 +45,17 @@ public function put($key, $value, $ttl = null) } return $result; - } else { - return $this->forget($key); } + + return $this->forget($key); } /** * Saves array of key value pairs to the cache * - * @param array $values * @param \DateTimeInterface|\DateInterval|float|int $ttl - * @return void */ - public function putMany(array $values, $ttl = null) + public function putMany(array $values, $ttl = null): void { foreach ($values as $key => $value) { $this->put($key, $value, $ttl); @@ -66,11 +64,9 @@ public function putMany(array $values, $ttl = null) /** * Flushes the cache for the given tags - * - * @return void */ - public function flush() + public function flush(): void { - return $this->store->flushByTags($this->tags); + $this->store->flushByTags($this->tags); } } diff --git a/src/Store.php b/src/Store.php index 5d758f3..71ec693 100644 --- a/src/Store.php +++ b/src/Store.php @@ -3,13 +3,13 @@ namespace ForFit\Mongodb\Cache; use Closure; -use Illuminate\Support\InteractsWithTime; use Illuminate\Cache\RetrievesMultipleKeys; -use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Cache\Store as StoreInterface; -use Jenssegers\Mongodb\Query\Builder; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Support\InteractsWithTime; use MongoDB\BSON\UTCDateTime; use MongoDB\Driver\Exception\BulkWriteException; +use MongoDB\Laravel\Query\Builder; class Store implements StoreInterface { @@ -59,7 +59,7 @@ public function get($key) { $cacheData = $this->table()->where('key', $this->getKeyWithPrefix($key))->first(); - return $cacheData ? unserialize($cacheData['value']) : null; + return $cacheData ? unserialize($cacheData->value) : null; } /** @@ -107,7 +107,7 @@ public function decrement($key, $value = 1) /** * @inheritDoc */ - public function forever($key, $value) + public function forever($key, $value): bool { return $this->put($key, $value, 315360000); } @@ -115,7 +115,7 @@ public function forever($key, $value) /** * @inheritDoc */ - public function forget($key) + public function forget($key): bool { $this->table()->where('key', '=', $this->getPrefix() . $key)->delete(); @@ -125,7 +125,7 @@ public function forget($key) /** * @inheritDoc */ - public function flush() + public function flush(): bool { $this->table()->delete(); @@ -135,29 +135,23 @@ public function flush() /** * @inheritDoc */ - public function getPrefix() + public function getPrefix(): string { return $this->prefix; } /** * Sets the tags to be used - * - * @param array $tags - * @return MongoTaggedCache */ - public function tags(array $tags) + public function tags(array $tags): MongoTaggedCache { return new MongoTaggedCache($this, $tags); } /** * Deletes all records with the given tag - * - * @param array $tags - * @return void */ - public function flushByTags(array $tags) + public function flushByTags(array $tags): void { foreach ($tags as $tag) { $this->table()->where('tags', $tag)->delete(); @@ -166,19 +160,16 @@ public function flushByTags(array $tags) /** * Retrieve an item's expiration time from the cache by key. - * - * @param string $key - * @return null|float|int */ - public function getExpiration($key) + public function getExpiration($key): ?float { $cacheData = $this->table()->where('key', $this->getKeyWithPrefix($key))->first(); - if (empty($cacheData['expiration'])) { + if (empty($cacheData->expiration)) { return null; } - $expirationSeconds = $cacheData['expiration']->toDateTime()->getTimestamp(); + $expirationSeconds = $cacheData->expiration->toDateTime()->getTimestamp(); return round($expirationSeconds - $this->currentTime()); } @@ -199,7 +190,7 @@ protected function table() * @param string $key * @return string */ - protected function getKeyWithPrefix(string $key) + protected function getKeyWithPrefix(string $key): string { return $this->getPrefix() . $key; } @@ -210,7 +201,7 @@ protected function getKeyWithPrefix(string $key) * @param string $key * @param int $value * @param Closure $callback - * @return int|bool + * @return false|mixed */ protected function incrementOrDecrement($key, $value, Closure $callback) { diff --git a/tests/AdvancedCacheFeaturesTest.php b/tests/AdvancedCacheFeaturesTest.php new file mode 100644 index 0000000..6a56b76 --- /dev/null +++ b/tests/AdvancedCacheFeaturesTest.php @@ -0,0 +1,168 @@ +store = new Store( + DB::connection('mongodb'), + $this->table() + ); + + // Freeze time for consistent testing + Carbon::setTestNow(now()); + } + + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); // Clear test now + } + + #[Test] + public function it_can_increment_numeric_values(): void + { + // Arrange + $this->store->put('counter', 5, 60); + + // Act + $result = $this->store->increment('counter', 3); + + // Assert + $this->assertEquals(8, $result); + $this->assertEquals(8, $this->store->get('counter')); + } + + #[Test] + public function it_can_decrement_numeric_values(): void + { + // Arrange + $this->store->put('counter', 10, 60); + + // Act + $result = $this->store->decrement('counter', 3); + + // Assert + $this->assertEquals(7, $result); + $this->assertEquals(7, $this->store->get('counter')); + } + + #[Test] + public function it_returns_false_when_incrementing_non_existent_key(): void + { + // Act + $result = $this->store->increment('non-existent', 1); + + // Assert + $this->assertFalse($result); + } + + #[Test] + public function it_can_store_items_forever(): void + { + // Act + $result = $this->store->forever('permanent-key', 'permanent-value'); + + // Assert + $this->assertTrue($result); + $this->assertEquals('permanent-value', $this->store->get('permanent-key')); + + // Verify long expiration time (10 years minimum) + $expiration = $this->store->getExpiration('permanent-key'); + $this->assertNotNull($expiration); + $this->assertGreaterThanOrEqual(315360000, $expiration); // >= 10 years + } + + #[Test] + public function it_can_store_and_retrieve_arrays(): void + { + // Arrange + $data = ['name' => 'Test', 'values' => [1, 2, 3]]; + + // Act + $this->store->put('array-data', $data, 60); + + // Assert + $result = $this->store->get('array-data'); + $this->assertEquals($data, $result); + $this->assertIsArray($result); + $this->assertEquals([1, 2, 3], $result['values']); + } + + #[Test] + public function it_can_store_and_retrieve_objects(): void + { + // Arrange + $data = new \stdClass(); + $data->name = 'Test Object'; + $data->value = 123; + + // Act + $this->store->put('object-data', $data, 60); + + // Assert + $result = $this->store->get('object-data'); + $this->assertEquals($data, $result); + $this->assertInstanceOf(\stdClass::class, $result); + $this->assertEquals('Test Object', $result->name); + $this->assertEquals(123, $result->value); + } + + #[Test] + public function it_can_forget_specific_keys(): void + { + // Arrange + $this->store->put('forget-me', 'value', 60); + $this->store->put('keep-me', 'value', 60); + + // Assert item exists before forgetting + $this->assertEquals('value', $this->store->get('forget-me')); + + // Act + $result = $this->store->forget('forget-me'); + + // Assert + $this->assertTrue($result); + $this->assertNull($this->store->get('forget-me')); + $this->assertEquals('value', $this->store->get('keep-me')); + } + + #[Test] + public function it_can_flush_entire_cache(): void + { + // Arrange + $this->store->put('key1', 'value1', 60); + $this->store->put('key2', 'value2', 60); + $this->store->tags(['tag1'])->put('key3', 'value3', 60); + + // Act + $result = $this->store->flush(); + + // Assert + $this->assertTrue($result); + $this->assertNull($this->store->get('key1')); + $this->assertNull($this->store->get('key2')); + $this->assertNull($this->store->get('key3')); + + // Verify MongoDB collection is empty + $count = DB::connection('mongodb') + ->table($this->table()) + ->count(); + + $this->assertEquals(0, $count); + } +} diff --git a/tests/LaravelIntegrationTest.php b/tests/LaravelIntegrationTest.php new file mode 100644 index 0000000..282eafa --- /dev/null +++ b/tests/LaravelIntegrationTest.php @@ -0,0 +1,163 @@ +app['config']->set('cache.default', 'mongodb'); + $this->app['config']->set('cache.prefix', 'laravel_cache:'); + + // Ensure MongoDB cache store is defined + $this->app['config']->set('cache.stores.mongodb', [ + 'driver' => 'mongodb', + 'table' => $this->table(), + 'connection' => 'mongodb', + ]); + + // Resolve the cache manager to register the driver + $this->app->make('cache'); + + // Clear any existing cache data + Cache::flush(); + + // Freeze time for consistent testing + Carbon::setTestNow(now()); + } + + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); // Clear test now + } + + #[Test] + public function it_registers_mongodb_driver_with_laravel(): void + { + // Simply test if the MongoDB cache driver works + $uniqueKey = 'mongodb-driver-test-' . uniqid('', true); + $uniqueValue = 'test-value-' . uniqid('', true); + + // Store a value using the MongoDB driver + Cache::driver('mongodb')->put($uniqueKey, $uniqueValue, 60); + + // Retrieve it and check if it matches + $this->assertEquals($uniqueValue, Cache::driver('mongodb')->get($uniqueKey)); + } + + #[Test] + public function it_works_with_laravel_cache_facade(): void + { + // Act + Cache::put('facade-test', 'facade-value', 60); + + // Assert + $this->assertEquals('facade-value', Cache::get('facade-test')); + } + + #[Test] + public function it_supports_cache_helper_functions(): void + { + // Act - Using the global cache() helper + cache(['helper-test' => 'helper-value'], 60); + + // Assert + $this->assertEquals('helper-value', cache('helper-test')); + } + + #[Test] + public function it_supports_has_method(): void + { + // Arrange + Cache::put('exists-key', 'exists-value', 60); + + // Act & Assert + $this->assertTrue(Cache::has('exists-key')); + $this->assertFalse(Cache::has('doesnt-exist-key')); + } + + #[Test] + public function it_supports_add_method(): void + { + // Since we now call flush() in setUp, we need to ensure no conflicts + $uniqueKey = 'add-test-' . uniqid('', true); + + // Act & Assert - Adding to non-existent key succeeds + $this->assertTrue(Cache::add($uniqueKey, 'add-value', 60)); + $this->assertEquals('add-value', Cache::get($uniqueKey)); + + // Act & Assert - Adding to existing key fails + $this->assertFalse(Cache::add($uniqueKey, 'new-value', 60)); + $this->assertEquals('add-value', Cache::get($uniqueKey)); // Value remains unchanged + } + + #[Test] + public function it_supports_remember_method(): void + { + // Act - This should store the value + $value = rand(1000, 9999); + $result1 = Cache::remember('remember-test', 60, function () use ($value) { + return 'remembered-' . $value; + }); + + // Act - This should retrieve from cache without executing callback + $result2 = Cache::remember('remember-test', 60, function () { + return 'different-' . rand(1000, 9999); + }); + + // Assert values match and callback wasn't executed second time + $this->assertEquals($result1, $result2); + } + + #[Test] + public function it_supports_forever_method(): void + { + // Act + Cache::forever('forever-key', 'forever-value'); + + // Assert + $this->assertEquals('forever-value', Cache::get('forever-key')); + } + + #[Test] + public function it_supports_pull_method(): void + { + // Arrange + Cache::put('pull-key', 'pull-value', 60); + + // Act - Pull retrieves and removes in one operation + $result = Cache::pull('pull-key'); + + // Assert + $this->assertEquals('pull-value', $result); + $this->assertFalse(Cache::has('pull-key')); + } + + #[Test] + public function it_supports_counting_and_incrementing(): void + { + // Arrange + Cache::put('count-key', 3, 60); + + // Act + $result1 = Cache::increment('count-key'); + $result2 = Cache::increment('count-key', 2); + $result3 = Cache::decrement('count-key'); + + // Assert + $this->assertEquals(4, $result1); + $this->assertEquals(6, $result2); + $this->assertEquals(5, $result3); + $this->assertEquals(5, Cache::get('count-key')); + } +} diff --git a/tests/Models/Cache.php b/tests/Models/Cache.php deleted file mode 100644 index 8599471..0000000 --- a/tests/Models/Cache.php +++ /dev/null @@ -1,16 +0,0 @@ - 'array']; -} diff --git a/tests/Overrides/Builder.php b/tests/Overrides/Builder.php deleted file mode 100644 index edf548c..0000000 --- a/tests/Overrides/Builder.php +++ /dev/null @@ -1,33 +0,0 @@ -compileWheres(), $this->parseValues($values)); - } - - return parent::update($values); - } - - public function where($column, $operator = null, $value = null, $boolean = 'and') - { - if ($column === 'tags') { - return parent::where($column, 'like', "%$operator%"); - } - - return parent::where($column, $operator, $value, $boolean); - } - - public function first($columns = ['*']) - { - $result = (array)parent::first($columns); - - return $this->parseValues($result); - } -} diff --git a/tests/Overrides/BuilderHelpers.php b/tests/Overrides/BuilderHelpers.php deleted file mode 100644 index 841c1a8..0000000 --- a/tests/Overrides/BuilderHelpers.php +++ /dev/null @@ -1,287 +0,0 @@ -map(function ($item) { - if (is_array($item)) { - $item = json_encode($item); - } elseif ((bool)strtotime($item) !== false) { - $item = Carbon::createFromTimeString($item); - } - - return $item; - })->toArray(); - } - - /** - * Compile the where array. - * @return array - */ - protected function compileWheres(): array - { - // The wheres to compile. - $wheres = $this->wheres ?: []; - - // We will add all compiled wheres to this array. - $compiled = []; - - foreach ($wheres as $i => &$where) { - // Make sure the operator is in lowercase. - if (isset($where['operator'])) { - $where['operator'] = strtolower($where['operator']); - - // Operator conversions - $convert = [ - 'regexp' => 'regex', - 'elemmatch' => 'elemMatch', - 'geointersects' => 'geoIntersects', - 'geowithin' => 'geoWithin', - 'nearsphere' => 'nearSphere', - 'maxdistance' => 'maxDistance', - 'centersphere' => 'centerSphere', - 'uniquedocs' => 'uniqueDocs', - ]; - - if (array_key_exists($where['operator'], $convert)) { - $where['operator'] = $convert[$where['operator']]; - } - } - - // Convert id's. - if (isset($where['column']) && ($where['column'] == '_id' || Str::endsWith($where['column'], '._id'))) { - // Multiple values. - if (isset($where['values'])) { - foreach ($where['values'] as &$value) { - $value = $this->convertKey($value); - } - } // Single value. - elseif (isset($where['value'])) { - $where['value'] = $this->convertKey($where['value']); - } - } - - // Convert DateTime values to UTCDateTime. - if (isset($where['value'])) { - if (is_array($where['value'])) { - array_walk_recursive($where['value'], function (&$item, $key) { - if ($item instanceof \DateTime) { - $item = new UTCDateTime($item->format('Uv')); - } - }); - } else { - if ($where['value'] instanceof DateTime) { - $where['value'] = new UTCDateTime($where['value']->format('Uv')); - } - } - } elseif (isset($where['values'])) { - array_walk_recursive($where['values'], function (&$item, $key) { - if ($item instanceof DateTime) { - $item = new UTCDateTime($item->format('Uv')); - } - }); - } - - // The next item in a "chain" of wheres devices the boolean of the - // first item. So if we see that there are multiple wheres, we will - // use the operator of the next where. - if ($i == 0 && count($wheres) > 1 && $where['boolean'] == 'and') { - $where['boolean'] = $wheres[$i + 1]['boolean']; - } - - // We use different methods to compile different wheres. - $method = "compileWhere{$where['type']}"; - $result = $this->{$method}($where); - - // Wrap the where with an $or operator. - if ($where['boolean'] == 'or') { - $result = ['$or' => [$result]]; - } - - // If there are multiple wheres, we will wrap it with $and. This is needed - // to make nested wheres work. - elseif (count($wheres) > 1) { - $result = ['$and' => [$result]]; - } - - // Merge the compiled where with the others. - $compiled = array_merge_recursive($compiled, $result); - } - - return $compiled; - } - - /** - * @param array $where - * @return array - */ - protected function compileWhereAll(array $where): array - { - extract($where); - - return [$column => ['$all' => array_values($values)]]; - } - - /** - * @param array $where - * @return array - */ - protected function compileWhereBasic(array $where): array - { - extract($where); - - // Replace like or not like with a Regex instance. - if (in_array($operator, ['like', 'not like'])) { - if ($operator === 'not like') { - $operator = 'not'; - } else { - $operator = '='; - } - - // Convert to regular expression. - $regex = preg_replace('#(^|[^\\\])%#', '$1.*', preg_quote($value)); - - // Convert like to regular expression. - if (!Str::startsWith($value, '%')) { - $regex = '^' . $regex; - } - if (!Str::endsWith($value, '%')) { - $regex .= '$'; - } - - $value = new Regex($regex, 'i'); - } // Manipulate regexp operations. - elseif (in_array($operator, ['regexp', 'not regexp', 'regex', 'not regex'])) { - // Automatically convert regular expression strings to Regex objects. - if (!$value instanceof Regex) { - $e = explode('/', $value); - $flag = end($e); - $regstr = substr($value, 1, -(strlen($flag) + 1)); - $value = new Regex($regstr, $flag); - } - - // For inverse regexp operations, we can just use the $not operator - // and pass it a Regex instence. - if (Str::startsWith($operator, 'not')) { - $operator = 'not'; - } - } - - if (!isset($operator) || $operator == '=') { - $query = [$column => $value]; - } elseif (array_key_exists($operator, $this->conversion)) { - $query = [$column => [$this->conversion[$operator] => $value]]; - } else { - $query = [$column => ['$' . $operator => $value]]; - } - - return $query; - } - - /** - * @param array $where - * @return mixed - */ - protected function compileWhereNested(array $where) - { - extract($where); - - return $query->compileWheres(); - } - - /** - * @param array $where - * @return array - */ - protected function compileWhereIn(array $where): array - { - extract($where); - - return [$column => ['$in' => array_values($values)]]; - } - - /** - * @param array $where - * @return array - */ - protected function compileWhereNotIn(array $where): array - { - extract($where); - - return [$column => ['$nin' => array_values($values)]]; - } - - /** - * @param array $where - * @return array - */ - protected function compileWhereNull(array $where): array - { - $where['operator'] = '='; - $where['value'] = null; - - return $this->compileWhereBasic($where); - } - - /** - * @param array $where - * @return array - */ - protected function compileWhereNotNull(array $where): array - { - $where['operator'] = '!='; - $where['value'] = null; - - return $this->compileWhereBasic($where); - } - - /** - * @param array $where - * @return array - */ - protected function compileWhereBetween(array $where): array - { - extract($where); - - if ($not) { - return [ - '$or' => [ - [ - $column => [ - '$lte' => $values[0], - ], - ], - [ - $column => [ - '$gte' => $values[1], - ], - ], - ], - ]; - } - - return [ - $column => [ - '$gte' => $values[0], - '$lte' => $values[1], - ], - ]; - } - - /** - * @param array $where - * @return mixed - */ - protected function compileWhereRaw(array $where) - { - return $where['sql']; - } -} diff --git a/tests/StoreTest.php b/tests/StoreTest.php index 502f9d2..932f326 100644 --- a/tests/StoreTest.php +++ b/tests/StoreTest.php @@ -5,141 +5,141 @@ use ForFit\Mongodb\Cache\MongoTaggedCache; use ForFit\Mongodb\Cache\Store; use Illuminate\Support\Carbon; -use Tests\Models\Cache; +use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\Test; class StoreTest extends TestCase { - protected $store; + protected Store $store; protected function setUp(): void { parent::setUp(); - $this->store = new Store($this->connection(), $this->table()); + // Setup the store with a real connection + $this->store = new Store( + DB::connection('mongodb'), + $this->table() + ); - // Freeze time. + // Freeze time for consistent testing Carbon::setTestNow(now()); } - /** @test */ - public function it_stores_an_item_in_the_cache_for_given_time() + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); // Clear test now + } + + #[Test] + public function it_stores_an_item_in_the_cache_for_given_time(): void { // Act - $sut = $this->store->put('test-key', 'test-value', 3); + $result = $this->store->put('test-key', 'test-value', 3); // Assert - $this->assertTrue($sut); - $this->assertDatabaseHas($this->table(), [ - 'key' => 'test-key', - 'value' => serialize('test-value'), - 'expiration' => (now()->timestamp + 3) * 1000, - 'tags' => '[]' - ]); + $this->assertTrue($result); + + // Verify the item was stored + $cacheItem = DB::connection('mongodb') + ->table($this->table()) + ->where('key', 'test-key') + ->first(); + + $this->assertNotNull($cacheItem); + $this->assertEquals(serialize('test-value'), $cacheItem->value); } - /** @test */ - public function it_updates_an_item_in_the_cache_for_given_time() + #[Test] + public function it_updates_an_item_in_the_cache_for_given_time(): void { - Cache::create([ - 'key' => 'test-key', - 'value' => serialize('fake-value') - ]); - - // Act - $sut = $this->store->put('test-key', 'new-value', 3); + // Setup - add initial value + $this->store->put('test-key', 'initial-value', 3); + + // Act - update the value + $result = $this->store->put('test-key', 'new-value', 3); // Assert - $this->assertTrue($sut); - $this->assertDatabaseHas($this->table(), [ - 'key' => 'test-key', - 'value' => serialize('new-value'), - 'expiration' => (now()->timestamp + 3) * 1000, - 'tags' => '[]' - ]); + $this->assertTrue($result); + + // Verify the item was updated + $cacheItem = DB::connection('mongodb') + ->table($this->table()) + ->where('key', 'test-key') + ->first(); + + $this->assertNotNull($cacheItem); + $this->assertEquals(serialize('new-value'), $cacheItem->value); } - /** @test */ - public function it_retrieves_value_from_the_cache_by_given_key() + #[Test] + public function it_retrieves_value_from_the_cache_by_given_key(): void { - // Arrange - Cache::create([ - 'key' => 'test-key', - 'value' => serialize('test-value') - ]); + // Setup - store a value + $this->store->put('test-key', 'test-value', 3); // Act - $sut = $this->store->get('test-key'); + $result = $this->store->get('test-key'); // Assert - $this->assertIsString($sut); - $this->assertEquals('test-value', $sut); + $this->assertEquals('test-value', $result); } - /** @test */ - public function it_returns_null_if_key_does_not_exist() + #[Test] + public function it_returns_null_if_key_does_not_exist(): void { // Act - $sut = $this->store->get('test-key'); + $result = $this->store->get('non-existent-key'); // Assert - $this->assertNull($sut); + $this->assertNull($result); } - /** @test */ - public function it_sets_the_tags_to_be_used() + #[Test] + public function it_sets_the_tags_to_be_used(): void { // Act - $sut = $this->store->tags(['tag1', 'tag2']); + $result = $this->store->tags(['tag1', 'tag2']); // Assert - $this->assertInstanceOf(MongoTaggedCache::class, $sut); - $this->assertPropertySame(['tag1', 'tag2'], 'tags', $sut); + $this->assertInstanceOf(MongoTaggedCache::class, $result); + + // Use reflection to test the tags property + $reflection = new \ReflectionObject($result); + $property = $reflection->getProperty('tags'); + $this->assertEquals(['tag1', 'tag2'], $property->getValue($result)); } - /** @test */ - public function it_deletes_all_records_with_the_given_tag() + #[Test] + public function it_deletes_all_records_with_the_given_tag(): void { - // Arrange - Cache::create([ - 'key' => 'test-key-1', - 'value' => serialize('test-value-1'), - 'tags' => ['tag1'] - ]); - - Cache::create([ - 'key' => 'test-key-2', - 'value' => serialize('test-value-2'), - 'tags' => ['tag2'] - ]); + // Setup + $this->store->tags(['tag1'])->put('key1', 'value1', 60); + $this->store->tags(['tag2'])->put('key2', 'value2', 60); + $this->store->tags(['tag1', 'tag2'])->put('key3', 'value3', 60); // Act $this->store->flushByTags(['tag1']); - + // Assert - $this->assertDatabaseMissing($this->table(), [ - 'key' => 'test-key-1', - 'value' => serialize('test-value-1'), - ]); - $this->assertDatabaseHas($this->table(), [ - 'key' => 'test-key-2', - 'value' => serialize('test-value-2'), - ]); + $this->assertNull($this->store->get('key1')); + $this->assertEquals('value2', $this->store->get('key2')); + $this->assertNull($this->store->get('key3')); } - /** @test */ - public function it_retrieves_an_items_expiration_time_by_given_key() + #[Test] + public function it_retrieves_an_items_expiration_time_by_given_key(): void { - // Arrange - Cache::create([ - 'key' => 'test-key', - 'value' => serialize('test-value'), - 'expiration' => now()->addDays(2) - ]); + // Setup with expiration time - 2 days + $this->store->put('test-key', 'test-value', 172800); // Act - $sut = $this->store->getExpiration('test-key'); + $result = $this->store->getExpiration('test-key'); - // Assert - $this->assertEquals(172800, $sut); // 2 days in seconds. + // Assert - approximately 2 days in seconds (172800) + // Allow for small differences in timing during test execution + $this->assertGreaterThan(172700, $result); + $this->assertLessThan(172900, $result); } } diff --git a/tests/TaggedCacheTest.php b/tests/TaggedCacheTest.php new file mode 100644 index 0000000..f409f1a --- /dev/null +++ b/tests/TaggedCacheTest.php @@ -0,0 +1,169 @@ +store = new Store( + DB::connection('mongodb'), + $this->table() + ); + + // Freeze time for consistent testing + Carbon::setTestNow(now()); + } + + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); // Clear test now + } + + #[Test] + public function it_can_store_multiple_items_with_tags(): void + { + // Arrange + $taggedCache = $this->store->tags(['api', 'v1']); + + // Act + $taggedCache->putMany([ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3' + ], 60); + + // Assert - The values are retrievable from the tagged cache + $this->assertEquals('value1', $taggedCache->get('key1')); + $this->assertEquals('value2', $taggedCache->get('key2')); + $this->assertEquals('value3', $taggedCache->get('key3')); + + // Assert - They're also retrievable from the base cache + $this->assertEquals('value1', $this->store->get('key1')); + $this->assertEquals('value2', $this->store->get('key2')); + $this->assertEquals('value3', $this->store->get('key3')); + } + + #[Test] + public function it_can_flush_by_specific_tag(): void + { + // Arrange - Create items with different combinations of tags + $this->store->tags(['api'])->put('api-only', 'api-value', 60); + $this->store->tags(['v1'])->put('v1-only', 'v1-value', 60); + $this->store->tags(['api', 'v1'])->put('api-v1', 'both-value', 60); + $this->store->tags(['other'])->put('other-tag', 'other-value', 60); + $this->store->put('no-tag', 'no-tag-value', 60); + + // Act - Flush only the 'api' tag + $this->store->flushByTags(['api']); + + // Assert - 'api' tagged items should be gone + $this->assertNull($this->store->get('api-only')); + $this->assertNull($this->store->get('api-v1')); + + // Assert - Other items should remain + $this->assertEquals('v1-value', $this->store->get('v1-only')); + $this->assertEquals('other-value', $this->store->get('other-tag')); + $this->assertEquals('no-tag-value', $this->store->get('no-tag')); + } + + #[Test] + public function it_can_flush_all_tagged_items(): void + { + // Arrange + $taggedCache = $this->store->tags(['api', 'v1']); + $taggedCache->put('tagged1', 'value1', 60); + $taggedCache->put('tagged2', 'value2', 60); + + // Add untagged item + $this->store->put('untagged', 'untagged-value', 60); + + // Act - Flush the tagged cache + $taggedCache->flush(); + + // Assert - Tagged items should be gone + $this->assertNull($this->store->get('tagged1')); + $this->assertNull($this->store->get('tagged2')); + + // Assert - Untagged item should remain + $this->assertEquals('untagged-value', $this->store->get('untagged')); + } + + #[Test] + public function it_can_put_forever_with_tags(): void + { + // Arrange + $taggedCache = $this->store->tags(['permanent']); + + // Act + $taggedCache->forever('permanent-tagged', 'permanent-value'); + + // Assert + $this->assertEquals('permanent-value', $taggedCache->get('permanent-tagged')); + } + + #[Test] + public function it_respects_cache_ttl_with_tags(): void + { + // Arrange - store a value with a short TTL (1 second) + $taggedCache = $this->store->tags(['expiring']); + $taggedCache->put('expires-soon', 'expiring-value', 1); + + // Assert - Value exists right after storing + $this->assertEquals('expiring-value', $taggedCache->get('expires-soon')); + + // Wait for expiration + sleep(2); + + // MongoDB TTL index runs approximately every 60 seconds + // So we can't reliably test automatic deletion + // Let's verify via a direct collection check if possible + $count = DB::connection('mongodb') + ->table($this->table()) + ->where('key', 'expires-soon') + ->count(); + + // If it's 0, great - it was removed + // If not, we can still check the expiration time is in the past + if ($count > 0) { + $cacheItem = DB::connection('mongodb') + ->table($this->table()) + ->where('key', 'expires-soon') + ->first(); + + $expireAt = $cacheItem->expiration->toDateTime()->getTimestamp(); + $this->assertLessThanOrEqual(time(), $expireAt, 'Expiration time should be in the past'); + } else { + $this->assertTrue(true, 'Item was automatically removed by MongoDB TTL index'); + } + } + + #[Test] + public function it_can_increment_and_decrement_with_tags(): void + { + // Arrange + $taggedCache = $this->store->tags(['counters']); + $taggedCache->put('counter', 5, 60); + + // Act & Assert - Increment + $this->assertEquals(8, $taggedCache->increment('counter', 3)); + $this->assertEquals(8, $taggedCache->get('counter')); + + // Act & Assert - Decrement + $this->assertEquals(6, $taggedCache->decrement('counter', 2)); + $this->assertEquals(6, $taggedCache->get('counter')); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 2f4c318..8e6c96c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,15 +3,15 @@ namespace Tests; use ForFit\Mongodb\Cache\ServiceProvider as MongoDbCacheServiceProvider; -use Illuminate\Database\ConnectionInterface; -use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; +use MongoDB\Laravel\MongoDBServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; -use Tests\Overrides\Builder; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; +#[RequiresPhpExtension('mongodb')] abstract class TestCase extends Orchestra { - private $table = 'cache_test'; - private $connectionInterface; + private string $table = 'cache_test'; /** * @return void @@ -20,28 +20,16 @@ protected function setUp(): void { parent::setUp(); - $this->setUpDatabase($this->app); - $this->connectionInterface = $this->initializeConnection(); + // Clear the cache collection before each test + $this->flushCache(); } /** - * @param \Illuminate\Foundation\Application $app - * - * @return array - */ - protected function getPackageProviders($app): array - { - return [ - MongoDbCacheServiceProvider::class, - ]; - } - - /** - * Set up the environment. + * Define environment setup. * * @param \Illuminate\Foundation\Application $app */ - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app): void { $app['config']->set('cache.stores.mongodb', [ 'driver' => 'mongodb', @@ -51,18 +39,29 @@ protected function getEnvironmentSetUp($app) $app['config']->set('database.default', 'mongodb'); $app['config']->set('database.connections.mongodb', [ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => '', + 'driver' => 'mongodb', + 'host' => env('MONGODB_HOST', '127.0.0.1'), + 'port' => env('MONGODB_PORT', 27017), + 'database' => env('MONGODB_DATABASE', 'laravel_mongodb_cache_test'), + 'username' => env('MONGODB_USERNAME', ''), + 'password' => env('MONGODB_PASSWORD', ''), + 'options' => [ + 'database' => env('MONGODB_AUTHENTICATION_DATABASE', 'admin'), + ], ]); } /** - * @return \Illuminate\Database\Connection|\Mockery\LegacyMockInterface|\Mockery\MockInterface|null + * @param \Illuminate\Foundation\Application $app + * + * @return array */ - protected function connection() + protected function getPackageProviders($app): array { - return $this->connectionInterface->getMock(); + return [ + MongoDbCacheServiceProvider::class, + MongoDBServiceProvider::class + ]; } /** @@ -74,50 +73,21 @@ protected function table(): string } /** - * Set up the database. - * - * @param \Illuminate\Foundation\Application $app + * Flush the cache collection */ - protected function setUpDatabase($app) + protected function flushCache(): void { - $app['db']->connection()->getSchemaBuilder()->create($this->table, function (Blueprint $table) { - $table->increments('_id'); - $table->dateTimeTz('expiration')->nullable(); - $table->string('key'); - $table->string('value'); - $table->json('tags')->nullable(); - }); + DB::connection('mongodb') + ->table($this->table) + ->delete(); } /** - * @return \Mockery\Expectation|\Mockery\ExpectationInterface|\Mockery\HigherOrderMessage + * Get the cache collection instance from MongoDB */ - protected function initializeConnection() + protected function getCacheCollection() { - $builder = app(Builder::class, ['connection' => $this->getConnection()]); - $builder->from = $this->table; - - return $this->spy(ConnectionInterface::class) - ->shouldReceive('table') - ->andReturn($builder); - } - - /** - * Assert the object has given property. - * - * @param $expected - * @param $property - * @param $object - * @param string $message - * @return void - * @throws \ReflectionException - */ - protected function assertPropertySame($expected, $property, $object, string $message = '') - { - $reflectedClass = new \ReflectionClass($object); - $reflection = $reflectedClass->getProperty($property); - $reflection->setAccessible(true); - - $this->assertSame($expected, $reflection->getValue($object), $message); + return DB::connection('mongodb') + ->table($this->table); } }