clearContentTables(); $this->app->detectEnvironment(fn (): string => 'production'); $this->runContentSeeder(); $this->assertSame(0, SiteSetting::query()->count()); $this->assertSame(0, Service::query()->count()); $this->assertSame(0, PortfolioCase::query()->count()); $this->assertSame(0, PortfolioImage::query()->count()); $this->assertSame(0, Testimonial::query()->count()); Storage::disk('public')->assertMissing('content/about/about-image.jpg'); Storage::disk('public')->assertMissing('content/og/og-default.jpg'); } public function test_content_seeder_does_not_clobber_owner_edited_content_in_production(): void { Storage::fake('public'); $siteSetting = SiteSetting::query()->create([ 'brand_name' => 'Nome escolhido pela dona', 'hero_eyebrow' => 'Eyebrow original', 'hero_title' => 'Título original', 'hero_subtitle' => 'Subtítulo original', 'hero_cta_label' => 'CTA original', 'hero_secondary_cta_label' => 'CTA secundário original', 'hero_note' => 'Nota original', 'about_summary' => 'Resumo original', 'manifesto_title' => 'Manifesto original', 'manifesto_lead' => 'Lead original', 'manifesto_body' => 'Corpo original', 'method_intro' => 'Intro original', 'method_steps' => [['title' => 'Passo único', 'body' => 'Descrição']], 'principles' => ['Princípio único'], 'email' => 'dona@amare.example', 'phone' => '(11) 90000-0000', 'city' => 'Fortaleza - CE', 'social_links' => ['instagram' => 'https://instagram.com/dona'], 'default_meta_title' => 'Meta original', 'default_meta_description' => 'Meta descrição original', 'analytics_enabled' => true, 'analytics_script' => '', ]); $service = Service::query()->create([ 'title' => 'Serviço editado pela dona', 'slug' => 'casamentos', 'summary' => 'Resumo editado', 'description' => 'Descrição editada', 'sort_order' => 99, 'is_featured' => false, 'published_at' => null, ]); $case = PortfolioCase::query()->create([ 'title' => 'Caso despublicado pela dona', 'slug' => 'casamento-ana-lucas', 'summary' => 'Resumo editado', 'event_type' => 'Casamento', 'city' => 'Fortaleza', 'venue' => 'Local editado', 'event_date' => '2024-01-01', 'challenge' => 'Desafio editado', 'solution' => 'Solução editada', 'result' => 'Resultado editado', 'cover_image_path' => 'content/portfolio/dona-cover.jpg', 'cover_image_alt' => 'Capa editada', 'is_featured' => false, 'sort_order' => 99, 'published_at' => null, ]); $image = PortfolioImage::query()->create([ 'portfolio_case_id' => $case->id, 'path' => 'content/portfolio/foto-real-da-dona.jpg', 'alt_text' => 'Foto real enviada pela dona', 'caption' => 'Momento real', 'sort_order' => 1, ]); $testimonial = Testimonial::query()->create([ 'quote' => 'Depoimento real não relacionado.', 'author_name' => 'Jeniffer e Maick', 'context' => 'Casamento · contexto original', 'sort_order' => 1, 'is_featured' => false, 'published_at' => null, ]); $siteSettingBefore = $siteSetting->fresh()?->getAttributes(); $serviceBefore = $service->fresh()?->getAttributes(); $caseBefore = $case->fresh()?->getAttributes(); $imageBefore = $image->fresh()?->getAttributes(); $testimonialBefore = $testimonial->fresh()?->getAttributes(); $countsBefore = $this->contentCounts(); $this->app->detectEnvironment(fn (): string => 'production'); // Run twice: production gating must be stable across repeated deploys. $this->runContentSeeder(); $this->runContentSeeder(); // Compare against counts captured just above, not hardcoded literals: // this table may carry rows created by other tests in the same // process (RefreshDatabase isolates per test method, not per model // globally), so the meaningful assertion is "the seeder created // nothing", not "there is exactly one row". $this->assertSame($countsBefore, $this->contentCounts()); $this->assertSame($siteSettingBefore, $siteSetting->fresh()?->getAttributes()); $this->assertSame($serviceBefore, $service->fresh()?->getAttributes()); $this->assertSame($caseBefore, $case->fresh()?->getAttributes()); $this->assertSame($imageBefore, $image->fresh()?->getAttributes()); $this->assertSame($testimonialBefore, $testimonial->fresh()?->getAttributes()); Storage::disk('public')->assertMissing('content/about/about-image.jpg'); Storage::disk('public')->assertMissing('content/og/og-default.jpg'); } /** * @return array */ public static function allowListedEnvironments(): array { return [ 'local' => ['local'], 'staging' => ['staging'], 'testing' => ['testing'], ]; } #[DataProvider('allowListedEnvironments')] public function test_content_seeder_still_seeds_demo_content_on_allow_listed_environments(string $environment): void { Storage::fake('public'); $this->clearContentTables(); $this->app->detectEnvironment(fn (): string => $environment); $this->runContentSeeder(); $this->assertSame(1, SiteSetting::query()->count()); $this->assertSame(3, Service::query()->count()); $this->assertSame(3, PortfolioCase::query()->count()); $this->assertSame(5, Testimonial::query()->count()); } /** * @return array */ public static function nonAllowListedEnvironments(): array { return [ 'production' => ['production'], 'empty string' => [''], 'mixed case Production' => ['Production'], 'upper case PRODUCTION' => ['PRODUCTION'], 'trailing whitespace' => ['staging '], 'unrecognized arbitrary value' => ['whatever-typo'], ]; } /** * The guard is an allow-list of known-safe environments, not a deny-list * of the single literal 'production'. Any value that is not exactly * 'local', 'staging', or 'testing' — including a blank APP_ENV, a * different case, stray whitespace, or a plain typo — must fail closed * (no-op) rather than fail open (seed/overwrite data). */ #[DataProvider('nonAllowListedEnvironments')] public function test_content_seeder_is_a_noop_on_any_non_allow_listed_environment(string $environment): void { Storage::fake('public'); $this->clearContentTables(); $this->app->detectEnvironment(fn (): string => $environment); $this->runContentSeeder(); $this->assertSame(0, SiteSetting::query()->count()); $this->assertSame(0, Service::query()->count()); $this->assertSame(0, PortfolioCase::query()->count()); $this->assertSame(0, PortfolioImage::query()->count()); $this->assertSame(0, Testimonial::query()->count()); } /** * Explicit clean slate for scenarios whose assertions depend on an * absolute, empty starting state ("empty production database" / "fresh * demo seed produces exactly N records"). * * This compensates for a pre-existing, out-of-scope test-isolation gap in * this suite: RefreshDatabase is expected to roll back every test's * writes, but a `SiteSetting` row created by `HomePageTest` (via * `SiteSetting::instance()`) has been observed to survive into * whichever test runs next when the full Feature suite executes * sequentially (reproduced with a minimal probe test asserting * `SiteSetting::query()->count() === 0`, which passes under `--filter` * but fails as part of `--testsuite=Feature`). Prime suspect: `tests/Pest.php` * applies `RefreshDatabase` suite-wide via * `pest()->extend(TestCase::class)->use(RefreshDatabase::class)->in('Feature')` * while several class-based tests (e.g. `HomePageTest`) also declare * `use RefreshDatabase;` themselves — double application is worth * checking first. Not fixed here: it is unrelated to MAN-103 and would * grow this diff well beyond the seeder-gating fix. */ private function clearContentTables(): void { PortfolioImage::query()->delete(); PortfolioCase::query()->delete(); Service::query()->delete(); Testimonial::query()->delete(); SiteSetting::query()->delete(); } /** * @return array{siteSettings: int, services: int, portfolioCases: int, portfolioImages: int, testimonials: int} */ private function contentCounts(): array { return [ 'siteSettings' => SiteSetting::query()->count(), 'services' => Service::query()->count(), 'portfolioCases' => PortfolioCase::query()->count(), 'portfolioImages' => PortfolioImage::query()->count(), 'testimonials' => Testimonial::query()->count(), ]; } private function runContentSeeder(): void { $this->artisan('db:seed', [ '--class' => ContentSeeder::class, '--force' => true, '--no-interaction' => true, ])->assertExitCode(0); } }