> bound =
+ Binder.get(env)
+ .bind("ca-skeleton.authz", Bindable.of(RolePermissionPolicy.class))
+ .map(RolePermissionPolicy::rolePermissions)
+ .orElse(Map.of());
+ assertThat(bound).containsEntry("site-admin", List.of("studio:read", "studio:write"));
+ }
+
+ @Test
+ void grantsBothReadAndWriteToTheConfiguredRole() {
+ MockEnvironment env = new MockEnvironment();
+ env.setProperty("app.studio.author-role", "site-admin");
+
+ epp.postProcessEnvironment(env, new SpringApplication());
+
+ assertThat(bind(env)).containsEntry("site-admin", List.of("studio:read", "studio:write"));
+ }
+
+ /**
+ * application.yml 이 선언하는 빈 맵을 재현한다. 이것이 매핑을 가리면 Studio 의 모든 쓰기가 403 이 된다 — 읽기는 통과하는데 쓰기만 막히는,
+ * 진단하기 어려운 모양으로 나타난다.
+ */
+ @Test
+ void survivesAnEmptyMapDeclaredByTheApplicationDefaults() {
+ MockEnvironment env = new MockEnvironment();
+ env.setProperty("app.studio.author-role", "site-admin");
+ env.getPropertySources()
+ .addLast(
+ new MapPropertySource(
+ "applicationDefaults", Map.of("ca-skeleton.authz.role-permissions", "")));
+
+ epp.postProcessEnvironment(env, new SpringApplication());
+
+ assertThat(bind(env)).containsEntry("site-admin", List.of("studio:read", "studio:write"));
+ }
+}
diff --git a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/techlog/StudioAuthzWiringTest.java b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/techlog/StudioAuthzWiringTest.java
new file mode 100644
index 0000000..bf98bc8
--- /dev/null
+++ b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/techlog/StudioAuthzWiringTest.java
@@ -0,0 +1,76 @@
+package dev.caskeleton.bootstrap.techlog;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import dev.caskeleton.adapter.inbound.web.authz.AuthorizationAdapter;
+import dev.caskeleton.adapter.inbound.web.authz.RolePermissionPolicy;
+import dev.caskeleton.adapter.inbound.web.authz.RolePermissionRegistry;
+import dev.caskeleton.application.security.AuthorizationPort;
+import dev.caskeleton.shared.security.Permission;
+import java.util.Set;
+import org.aopalliance.intercept.MethodInvocation;
+import org.junit.jupiter.api.Test;
+import org.springframework.aop.Advisor;
+import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Role;
+import org.springframework.mock.env.MockEnvironment;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
+
+/**
+ * 후처리기가 심은 매핑이 실행 중인 컨텍스트에서도 살아 있는지 본다.
+ *
+ * 바인딩만 따로 확인하면 통과하지만 앱에서는 죽는 경우가 있어서다. {@code MethodSecurityConfig} 의 advisor 는 auto-proxy 보다 먼저
+ * 만들어져야 하는 인프라 빈인데 {@link AuthorizationPort} 를 생성자 파라미터로 받는다. 그래서 {@code AuthorizationAdapter →
+ * RolePermissionRegistry → RolePermissionPolicy} 가 BeanPostProcessor 등록이 끝나기 전에 끌려 올라온다 — 운영 로그가 이
+ * 세 빈에 대해 "not eligible for getting processed by all BeanPostProcessors" 를 정확히 그렇게 찍고 있다.
+ */
+class StudioAuthzWiringTest {
+
+ @Configuration(proxyBeanMethods = false)
+ @EnableConfigurationProperties(RolePermissionPolicy.class)
+ static class Wiring {
+
+ @Bean
+ RolePermissionRegistry rolePermissionRegistry(RolePermissionPolicy policy) {
+ return new RolePermissionRegistry(policy);
+ }
+
+ @Bean
+ AuthorizationPort authorizationAdapter(RolePermissionRegistry registry) {
+ return new AuthorizationAdapter(registry);
+ }
+
+ /** MethodSecurityConfig 와 같은 모양: 인프라 advisor 가 포트를 직접 받는다. */
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ static Advisor requiresPermissionAuthorizationAdvisor(AuthorizationPort authorizationPort) {
+ AuthorizationManager manager = (authentication, invocation) -> null;
+ return new AuthorizationManagerBeforeMethodInterceptor(
+ AnnotationMatchingPointcut.forMethodAnnotation(Deprecated.class), manager);
+ }
+ }
+
+ @Test
+ void theConfiguredRoleKeepsBothPermissionsInsideARunningContext() {
+ MockEnvironment env = new MockEnvironment();
+ env.setProperty("app.studio.author-role", "site-admin");
+ new StudioAuthzEnvironmentPostProcessor().postProcessEnvironment(env, new SpringApplication());
+
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ context.setEnvironment(env);
+ context.register(Wiring.class);
+ context.refresh();
+
+ RolePermissionRegistry registry = context.getBean(RolePermissionRegistry.class);
+ assertThat(registry.effectivePermissions(Set.of("site-admin")))
+ .contains(Permission.parse("studio:read"), Permission.parse("studio:write"));
+ }
+ }
+}