import com.correx.core.approvals.ApprovalOutcome import com.correx.core.approvals.ApprovalStatus import com.correx.core.approvals.GrantScope import com.correx.core.approvals.Tier import com.correx.core.approvals.domain.DefaultApprovalEngine import com.correx.core.approvals.model.ApprovalContext import com.correx.core.approvals.model.ApprovalGrant import com.correx.core.approvals.model.ApprovalScopeIdentity import com.correx.core.events.types.GrantId import com.correx.core.events.types.SessionId import com.correx.core.sessions.ApprovalMode import com.correx.testing.fixtures.request import kotlinx.datetime.Instant import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test class GrantSecurityTest { private val engine = DefaultApprovalEngine() private val now = Instant.parse("2026-01-01T00:00:01Z") private val grantTime = Instant.parse("2026-01-01T00:00:00Z") private fun denyCtx() = ApprovalContext( ApprovalScopeIdentity(SessionId("s1"), null, null), ApprovalMode.DENY ) // ─── 1. Operation isolation ─────────────────────────────────────────────── @Test fun `SESSION grant for shell does not auto-approve write_file request`() { val req = request("r1", Tier.T2).copy(toolName = "write_file") val grant = ApprovalGrant( id = GrantId("g1"), scope = GrantScope.SESSION(toolName = "shell"), permittedTiers = setOf(Tier.T2), reason = "test", timestamp = grantTime ) val decision = engine.evaluate(req, denyCtx(), listOf(grant), now) assertEquals(ApprovalStatus.PENDING, decision.state) assertNull(decision.outcome) } @Test fun `SESSION grant for write_file does not auto-approve shell request`() { val req = request("r1", Tier.T2).copy(toolName = "shell") val grant = ApprovalGrant( id = GrantId("g1"), scope = GrantScope.SESSION(toolName = "write_file"), permittedTiers = setOf(Tier.T2), reason = "test", timestamp = grantTime ) val decision = engine.evaluate(req, denyCtx(), listOf(grant), now) assertEquals(ApprovalStatus.PENDING, decision.state) assertNull(decision.outcome) } // ─── 2. Tier ceiling ───────────────────────────────────────────────────── @Test fun `grant with max tier T2 does not auto-approve T3 request even with matching toolName`() { val req = request("r1", Tier.T3).copy(toolName = "shell") val grant = ApprovalGrant( id = GrantId("g1"), scope = GrantScope.SESSION(toolName = "shell"), permittedTiers = setOf(Tier.T2), reason = "test", timestamp = grantTime ) val decision = engine.evaluate(req, denyCtx(), listOf(grant), now) assertEquals(ApprovalStatus.PENDING, decision.state) assertNull(decision.outcome) } @Test fun `grant with max tier T2 auto-approves T2 request with matching toolName (positive control)`() { val req = request("r1", Tier.T2).copy(toolName = "shell") val grant = ApprovalGrant( id = GrantId("g1"), scope = GrantScope.SESSION(toolName = "shell"), permittedTiers = setOf(Tier.T2), reason = "test", timestamp = grantTime ) val decision = engine.evaluate(req, denyCtx(), listOf(grant), now) assertEquals(ApprovalOutcome.AUTO_APPROVED, decision.outcome) assertEquals(ApprovalStatus.COMPLETED, decision.state) } @Test fun `grant with max tier T2 auto-approves T1 request with matching toolName (ceiling covers lower tiers)`() { val req = request("r1", Tier.T1).copy(toolName = "shell") val grant = ApprovalGrant( id = GrantId("g1"), scope = GrantScope.SESSION(toolName = "shell"), permittedTiers = setOf(Tier.T2), reason = "test", timestamp = grantTime ) val decision = engine.evaluate(req, denyCtx(), listOf(grant), now) assertEquals(ApprovalOutcome.AUTO_APPROVED, decision.outcome) assertEquals(ApprovalStatus.COMPLETED, decision.state) } @Test fun `grant with max tier T2 auto-approves T0 request with matching toolName (ceiling covers lowest tier)`() { val req = request("r1", Tier.T0).copy(toolName = "shell") val grant = ApprovalGrant( id = GrantId("g1"), scope = GrantScope.SESSION(toolName = "shell"), permittedTiers = setOf(Tier.T2), reason = "test", timestamp = grantTime ) val decision = engine.evaluate(req, denyCtx(), listOf(grant), now) assertEquals(ApprovalOutcome.AUTO_APPROVED, decision.outcome) assertEquals(ApprovalStatus.COMPLETED, decision.state) } // ─── 3. No blanket grant ────────────────────────────────────────────────── @Test fun `SESSION grant with null toolName does not match any request (default-deny preserved)`() { val req = request("r1", Tier.T2).copy(toolName = "shell") val grant = ApprovalGrant( id = GrantId("g1"), scope = GrantScope.SESSION(toolName = null), permittedTiers = setOf(Tier.T2), reason = "test", timestamp = grantTime ) val decision = engine.evaluate(req, denyCtx(), listOf(grant), now) assertEquals(ApprovalStatus.PENDING, decision.state) assertNull(decision.outcome) } @Test fun `SESSION grant with null toolName does not match request with null toolName`() { val req = request("r1", Tier.T2).copy(toolName = null) val grant = ApprovalGrant( id = GrantId("g1"), scope = GrantScope.SESSION(toolName = null), permittedTiers = setOf(Tier.T2), reason = "test", timestamp = grantTime ) val decision = engine.evaluate(req, denyCtx(), listOf(grant), now) assertEquals(ApprovalStatus.PENDING, decision.state) assertNull(decision.outcome) } }