toBeInstanceOf(SitemapGenerator::class); }); it('generates an empty sitemap when no pages or cruds are configured', function () { /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ // GIVEN $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => [], 'supported_locales' => ['fr'], 'cruds' => [], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator($config, new SitemapHelper($config, new DefaultRouteLocalizer())); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap)->toBeInstanceOf(Sitemap::class) ->and($sitemap->getTags())->toBeEmpty(); }); it('generates a sitemap with single pages in a single locale', function () { // GIVEN Route::name('about')->get('{locale}/about', fn () => 'About'); Route::name('contact')->get('{locale}/contact', fn () => 'Contact'); /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => ['about', 'contact'], 'supported_locales' => ['fr'], 'cruds' => [], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator($config, new SitemapHelper($config, new DefaultRouteLocalizer())); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap)->toBeInstanceOf(Sitemap::class) ->and($sitemap->hasUrl('http://localhost/fr/about'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/fr/contact'))->toBeTrue(); }); it('generates a sitemap with single pages and alternates across multiple locales', function () { // GIVEN Route::name('about')->get('{locale}/about', fn () => 'About'); Route::name('contact')->get('{locale}/contact', fn () => 'Contact'); /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => ['about', 'contact'], 'supported_locales' => ['fr', 'en'], 'cruds' => [], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator($config, new SitemapHelper($config, new DefaultRouteLocalizer())); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap)->toBeInstanceOf(Sitemap::class) ->and($sitemap->hasUrl('http://localhost/fr/about'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/en/about'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/fr/contact'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/en/contact'))->toBeTrue(); $aboutTagFr = collect($sitemap->getTags())->first(fn (mixed $tag): bool => $tag instanceof Url && $tag->url === 'http://localhost/fr/about'); Assert::isInstanceOf($aboutTagFr, Url::class); expect($aboutTagFr->alternates)->toHaveCount(1) ->and($aboutTagFr->alternates[0]->url)->toBe('http://localhost/en/about') ->and($aboutTagFr->alternates[0]->locale)->toBe('en'); }); it('ignores non-existing routes gracefully', function () { // GIVEN Route::name('about')->get('{locale}/about', fn () => 'About'); /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => ['about', 'inexisting-route'], 'supported_locales' => ['fr'], 'cruds' => [], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator( $config, new SitemapHelper( $config, new DefaultRouteLocalizer() ) ); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap->hasUrl('http://localhost/fr/about'))->toBeTrue() ->and($sitemap->getTags())->toHaveCount(1); }); it('generates a sitemap with cruds models and alternates', function () { // GIVEN Schema::create('model_classes', function (Blueprint $table) { $table->id(); $table->timestamps(); }); ModelClass::create(); /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => [], 'supported_locales' => ['fr', 'en'], 'cruds' => [ [ 'model' => ModelClass::class, ], ], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator( $config, new SitemapHelper( $config, new DefaultRouteLocalizer() ) ); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap)->toBeInstanceOf(Sitemap::class) ->and($sitemap->hasUrl('http://localhost/fr/posts'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/en/posts'))->toBeTrue(); $crudTagFr = collect($sitemap->getTags()) ->first(fn (mixed $tag): bool => $tag instanceof Url && $tag->url === 'http://localhost/fr/posts'); Assert::isInstanceOf($crudTagFr, Url::class); expect($crudTagFr->alternates)->toHaveCount(1) ->and($crudTagFr->alternates[0]->url)->toBe('http://localhost/en/posts') ->and($crudTagFr->alternates[0]->locale)->toBe('en'); }); it('generates a combined sitemap with single pages and cruds', function () { // GIVEN Route::name('home')->get('{locale}/home', fn () => 'Home'); Schema::create('model_classes', function (Blueprint $table) { $table->id(); $table->timestamps(); }); ModelClass::create(); /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => ['home'], 'supported_locales' => ['fr'], 'cruds' => [ [ 'model' => ModelClass::class, ], ], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator( $config, new SitemapHelper( $config, new DefaultRouteLocalizer() ) ); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap->hasUrl('http://localhost/fr/home'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/fr/posts'))->toBeTrue() ->and($sitemap->getTags())->toHaveCount(2); }); it('generates sitemap with cruds matching documentation config example (model_filters, closure relation_filters, allow_null_relation, and multi-crud)', function () { // GIVEN Schema::create('ranges', function (Blueprint $table) { $table->id(); $table->boolean('status')->default(true); $table->timestamps(); }); Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('slug'); $table->boolean('published')->default(true); $table->foreignId('range_id')->nullable(); $table->timestamps(); }); Schema::create('articles', function (Blueprint $table) { $table->id(); $table->string('slug'); $table->timestamps(); }); $activeRange = Range::create(['status' => true]); $inactiveRange = Range::create(['status' => false]); Product::create(['slug' => 'p-active-range', 'published' => true, 'range_id' => $activeRange->id]); Product::create(['slug' => 'p-no-range', 'published' => true, 'range_id' => null]); Product::create(['slug' => 'p-inactive-range', 'published' => true, 'range_id' => $inactiveRange->id]); Product::create(['slug' => 'p-draft', 'published' => false, 'range_id' => $activeRange->id]); Article::create(['slug' => 'first-article']); /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => [], 'supported_locales' => ['fr'], 'cruds' => [ [ 'model' => Product::class, 'model_filters' => [ ['published', '=', true], ], 'relation_filters' => [ 'range' => fn (Builder $q) => $q->where('status', true), ], 'allow_null_relation' => ['range'], ], [ 'model' => Article::class, ], ], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator( $config, new SitemapHelper( $config, new DefaultRouteLocalizer() ) ); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap->hasUrl('http://localhost/fr/products/p-active-range'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/fr/products/p-no-range'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/fr/products/p-inactive-range'))->toBeFalse() ->and($sitemap->hasUrl('http://localhost/fr/products/p-draft'))->toBeFalse() ->and($sitemap->hasUrl('http://localhost/fr/articles/first-article'))->toBeTrue() ->and($sitemap->getTags())->toHaveCount(3); }); it('filters crud models requiring existing relation when relation is not in allow_null_relation', function () { // GIVEN Schema::create('ranges', function (Blueprint $table) { $table->id(); $table->boolean('status')->default(true); $table->timestamps(); }); Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('slug'); $table->boolean('published')->default(true); $table->foreignId('range_id')->nullable(); $table->timestamps(); }); $activeRange = Range::create(['status' => true]); Product::create(['slug' => 'with-range', 'published' => true, 'range_id' => $activeRange->id]); Product::create(['slug' => 'without-range', 'published' => true, 'range_id' => null]); /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => [], 'supported_locales' => ['fr'], 'cruds' => [ [ 'model' => Product::class, 'relation_filters' => [ 'range' => fn (Builder $q) => $q->where('status', true), ], 'allow_null_relation' => [], ], ], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator( $config, new SitemapHelper( $config, new DefaultRouteLocalizer() ) ); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap->hasUrl('http://localhost/fr/products/with-range'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/fr/products/without-range'))->toBeFalse() ->and($sitemap->getTags())->toHaveCount(1); }); it('filters crud models using relation_filters with array constraint', function () { // GIVEN Schema::create('ranges', function (Blueprint $table) { $table->id(); $table->boolean('status')->default(true); $table->timestamps(); }); Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('slug'); $table->boolean('published')->default(true); $table->foreignId('range_id')->nullable(); $table->timestamps(); }); $activeRange = Range::create(['status' => true]); $inactiveRange = Range::create(['status' => false]); Product::create(['slug' => 'in-active', 'published' => true, 'range_id' => $activeRange->id]); Product::create(['slug' => 'in-inactive', 'published' => true, 'range_id' => $inactiveRange->id]); /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => [], 'supported_locales' => ['fr'], 'cruds' => [ [ 'model' => Product::class, 'relation_filters' => [ 'range' => ['status' => true], ], ], ], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator( $config, new SitemapHelper( $config, new DefaultRouteLocalizer() ) ); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap->hasUrl('http://localhost/fr/products/in-active'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/fr/products/in-inactive'))->toBeFalse() ->and($sitemap->getTags())->toHaveCount(1); }); it('filters crud models using relation_filters with default string boolean constraint', function () { // GIVEN Schema::create('ranges', function (Blueprint $table) { $table->id(); $table->boolean('status')->default(true); $table->timestamps(); }); Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('slug'); $table->boolean('published')->default(true); $table->foreignId('range_id')->nullable(); $table->timestamps(); }); $activeRange = Range::create(['status' => true]); $inactiveRange = Range::create(['status' => false]); Product::create(['slug' => 'in-active', 'published' => true, 'range_id' => $activeRange->id]); Product::create(['slug' => 'in-inactive', 'published' => true, 'range_id' => $inactiveRange->id]); /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => [], 'supported_locales' => ['fr'], 'cruds' => [ [ 'model' => Product::class, 'relation_filters' => [ 'range' => 'status', ], ], ], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator( $config, new SitemapHelper( $config, new DefaultRouteLocalizer() ) ); // WHEN $sitemap = $generator->generate(); // THEN expect($sitemap->hasUrl('http://localhost/fr/products/in-active'))->toBeTrue() ->and($sitemap->hasUrl('http://localhost/fr/products/in-inactive'))->toBeFalse() ->and($sitemap->getTags())->toHaveCount(1); }); it('throws SitemapException when an error occurs during sitemap generation', function () { // GIVEN /** @var \Uwil\SitemapGenerator\Tests\TestCase $this */ $config = new SitemapConfig(new ConfigRepository([ SitemapConfig::CONFIG_KEY => [ 'single_pages' => [], 'supported_locales' => ['fr'], 'cruds' => [ [ // NOTE: This will cause an error because the 'products' table is not created 'model' => Product::class, ], ], 'schedule' => false, 'cron' => '* * * * *', 'localizer' => 'default', 'output' => [ 'storage_disk' => $this->outputDisk, 'path' => $this->outputPath, ], ], ])); $generator = new SitemapGenerator( $config, new SitemapHelper( $config, new DefaultRouteLocalizer() ) ); // WHEN $action = fn () => $generator->generate(); // THEN expect($action)->toThrow( SitemapException::class, 'An error occurred while generating the sitemap' ); }); });