정리 대상 디렉토리 */ private array $createdPaths = []; protected function tearDown(): void { AssetUrl::forceMode(null); foreach ($this->createdPaths as $path) { $this->deleteDirectory($path); } parent::tearDown(); } /** * 모듈 — 확장자 없는 모드에서 상대 참조가 치환되고 그 주소가 파일을 돌려준다. */ public function test_module_extensionless_mode_rewrites_and_resolves(): void { $identifier = $this->makeModule(); AssetUrl::forceMode(AssetUrl::MODE_EXTENSIONLESS); $css = $this->get("/api/modules/assets/{$identifier}?file=".rawurlencode('dist/css/style.css')) ->assertOk() ->getContent(); $fontUrl = "/api/modules/assets/{$identifier}?file=".rawurlencode('dist/woff2/f.woff2'); $this->assertStringContainsString($fontUrl, $css, '모듈 CSS 의 상대 참조가 치환되지 않았습니다.'); $this->assertStringNotContainsString("url('../woff2/f.woff2')", $css); // 왕복 — 문자열만 맞고 서빙이 404 면 화면 증상은 그대로다. $this->assertSame('FONTBYTES', $this->get($fontUrl)->assertOk()->streamedContent()); } /** * 모듈 — 확장자 모드도 같은 경로를 타고 경로 형태로 치환된다. */ public function test_module_extension_mode_rewrites_and_resolves(): void { $identifier = $this->makeModule(); AssetUrl::forceMode(AssetUrl::MODE_EXTENSION); $css = $this->get("/api/modules/assets/{$identifier}/dist/css/style.css") ->assertOk() ->getContent(); $fontUrl = "/api/modules/assets/{$identifier}/dist/woff2/f.woff2"; $this->assertStringContainsString($fontUrl, $css); $this->assertSame('FONTBYTES', $this->get($fontUrl)->assertOk()->streamedContent()); } /** * 플러그인 — 확장자 없는 모드에서 상대 참조가 치환되고 그 주소가 파일을 돌려준다. */ public function test_plugin_extensionless_mode_rewrites_and_resolves(): void { $identifier = $this->makePlugin(); AssetUrl::forceMode(AssetUrl::MODE_EXTENSIONLESS); $css = $this->get("/api/plugins/assets/{$identifier}?file=".rawurlencode('dist/css/style.css')) ->assertOk() ->getContent(); $fontUrl = "/api/plugins/assets/{$identifier}?file=".rawurlencode('dist/woff2/f.woff2'); $this->assertStringContainsString($fontUrl, $css, '플러그인 CSS 의 상대 참조가 치환되지 않았습니다.'); $this->assertStringNotContainsString("url('../woff2/f.woff2')", $css); $this->assertSame('FONTBYTES', $this->get($fontUrl)->assertOk()->streamedContent()); } /** * 플러그인 — 확장자 모드도 같은 경로를 타고 경로 형태로 치환된다. */ public function test_plugin_extension_mode_rewrites_and_resolves(): void { $identifier = $this->makePlugin(); AssetUrl::forceMode(AssetUrl::MODE_EXTENSION); $css = $this->get("/api/plugins/assets/{$identifier}/dist/css/style.css") ->assertOk() ->getContent(); $fontUrl = "/api/plugins/assets/{$identifier}/dist/woff2/f.woff2"; $this->assertStringContainsString($fontUrl, $css); $this->assertSame('FONTBYTES', $this->get($fontUrl)->assertOk()->streamedContent()); } /** * 절대·루트상대 참조는 두 확장 유형에서도 손대지 않는다. */ public function test_absolute_references_are_never_rewritten_for_extensions(): void { $identifier = $this->makeModule(); AssetUrl::forceMode(AssetUrl::MODE_EXTENSIONLESS); $css = $this->get("/api/modules/assets/{$identifier}?file=".rawurlencode('dist/css/style.css')) ->assertOk() ->getContent(); $this->assertStringContainsString('https://cdn.example.com/x.png', $css); $this->assertStringContainsString('/build/ext/1/y.png', $css); } /** * 활성 모듈과 그 자산 파일을 만듭니다. * * @return string 모듈 식별자 */ private function makeModule(): string { $identifier = 'test-css-module'; Module::factory()->create([ 'identifier' => $identifier, 'status' => ExtensionStatus::Active->value, ]); $this->writeAssets(base_path("modules/{$identifier}")); return $identifier; } /** * 활성 플러그인과 그 자산 파일을 만듭니다. * * @return string 플러그인 식별자 */ private function makePlugin(): string { $identifier = 'test-css-plugin'; Plugin::factory()->create([ 'identifier' => $identifier, 'status' => ExtensionStatus::Active->value, ]); $this->writeAssets(base_path("plugins/{$identifier}")); return $identifier; } /** * 확장 디렉토리에 CSS·서브리소스를 만듭니다. * * 상대 해석은 경로 계층이 있어야 의미가 있으므로 실제 디렉토리 구조를 만든다. * * @param string $root 확장 루트 절대 경로 */ private function writeAssets(string $root): void { $this->createdPaths[] = $root; @mkdir($root.'/dist/css', 0755, true); @mkdir($root.'/dist/woff2', 0755, true); file_put_contents( $root.'/dist/css/style.css', "@font-face{src:url('../woff2/f.woff2')}\n" ."b{background:url('https://cdn.example.com/x.png')}\n" ."c{background:url('/build/ext/1/y.png')}\n" ); file_put_contents($root.'/dist/woff2/f.woff2', 'FONTBYTES'); } /** * 디렉토리를 재귀 삭제합니다. * * @param string $dir 대상 디렉토리 */ private function deleteDirectory(string $dir): void { if (! is_dir($dir)) { return; } foreach (array_diff(scandir($dir) ?: [], ['.', '..']) as $item) { $path = $dir.'/'.$item; is_dir($path) ? $this->deleteDirectory($path) : @unlink($path); } @rmdir($dir); } }