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