A deployment policy can look restrictive in a pull request while one small edit quietly removes the condition that made it safe. That is especially easy to miss in a short Rego rule. An approval requirement, environment check, or immutable artifact check is often one expression among several. Reading the policy is necessary, but it is not a substitute for asking the policy engine whether the requests that must be refused are still refused.
This guide uses Open Policy Agent (OPA) 1.19.1 to place three deployment decisions under test: an approved staging change is allowed, a staging change without approval is denied, and a production request is denied. The successful result is not merely that Rego parses. opa test returns success for the intended policy, and it returns a nonzero status when a deliberate edit removes the approval condition. The fixture uses only synthetic input and a local executable; it does not contact a cluster, CI service, registry, or production policy endpoint.
OPA 1.19.1 was released on 17 August 2026. Its release notes say that it was built with Go 1.26.6 to address standard-library vulnerabilities used by OPA’s HTTP handler and crypto built-ins, while the remaining OPA code matches 1.19.0. That is a reason to record the OPA binary used by a policy test, particularly when the binary is delivered as a service or container. It is not evidence that an installation is vulnerable or that a source-built binary uses the same Go release.
Start with the decision, not the policy syntax
A useful test names the authorization boundary in operational terms. Here, the boundary is deliberately narrow: a deployment controller may proceed only when its input says staging, an approval is true, and an artifact digest is present. Production is excluded from this policy on purpose. A real production path might require a separate policy and additional change controls, but it should not become allowed merely because a staging rule was generalized.
Create the policy in a disposable directory. import rego.v1 makes the rule syntax explicit for OPA’s v1 language mode, and default allow := false gives a missing or incomplete input a deny result rather than an undefined decision.
package deploy.authz
import rego.v1
default allow := false
allow if {
input.environment == "staging"
input.change.approved == true
input.artifact.digest
}
The three expressions in the allow body are an AND condition. The input must satisfy all of them. The final expression only checks that a digest value is present; a production policy would normally also validate where that digest came from and which repository or signer is permitted. Keep that supply-chain decision in a policy designed for it rather than treating this small example as a complete deployment control.
The default matters when an input field is absent. Without it, a query can be undefined rather than explicitly false. An undefined result may be handled differently by an integration, depending on the query and its error handling. A deny-by-default rule gives the caller a stable authorization decision, but the integration must still be configured to fail closed if OPA itself is unavailable or returns an error.
Write both an allowed and denied assertion
Put tests in a separate file in the same package. OPA treats rules whose names begin with test_ as tests. The with input as expression replaces the policy input for one assertion, which keeps each request visible next to the expected decision.
package deploy.authz
import rego.v1
test_approved_staging_change_is_allowed if {
allow with input as {
"environment": "staging",
"change": {"approved": true},
"artifact": {"digest": "sha256:example"},
}
}
test_missing_approval_is_denied if {
not allow with input as {
"environment": "staging",
"change": {"approved": false},
"artifact": {"digest": "sha256:example"},
}
}
test_production_is_denied if {
not allow with input as {
"environment": "production",
"change": {"approved": true},
"artifact": {"digest": "sha256:example"},
}
}
A positive test alone is weak evidence. It can prove that the happy path works while a wildcard, a removed condition, or an incorrectly broad default allows requests that should fail. The two negative cases make the boundary concrete. Keep the inputs synthetic; policies and test fixtures sometimes accumulate repository names, internal environment labels, account IDs, and other deployment details that do not belong in a public example.
The test file uses not allow for the denial assertions. That is intentional: the test fails if the policy unexpectedly evaluates to true. Do not write a test that only queries allow and then rely on a human to notice a changed result in CI output. A policy regression needs to turn into a nonzero CI result before a deployment job consumes the decision.
Verify the downloaded binary before using it
Download the Linux AMD64 OPA binary and the adjacent checksum file from the same 1.19.1 release. Check the entry for the exact file before setting the executable bit:
set -eu
version=1.19.1
base="https://github.com/open-policy-agent/opa/releases/download/v${version}"
curl -fsSLo opa "$base/opa_linux_amd64"
curl -fsSLo opa_linux_amd64.sha256 "$base/opa_linux_amd64.sha256"
expected=$(cut -d' ' -f1 opa_linux_amd64.sha256)
printf '%s %s\n' "$expected" opa | sha256sum -c -
chmod 700 opa
./opa version
The checksum file contains a hash followed by the upstream filename. Extracting its first field is important when the local executable is named simply opa; passing the original checksum file straight to sha256sum -c would look for opa_linux_amd64 in the current directory. Do not bypass the check by renaming files until a verification command passes. Preserve the release URL, checksum, OPA version, operating system, and architecture with the change record.
In an isolated Linux AMD64 validation run, the 1.19.1 binary matched its published SHA-256 value and reported version 1.19.1. The policy passed opa check, and the three tests produced 100% line coverage for the small fixture. Coverage is useful for finding unvisited policy lines; it does not prove that the selected inputs represent every risky request. Add cases when a policy gains a new branch, exception, identity field, or external-data lookup.
Make the regression visible
Run the syntax check before the test suite, then run the tests in JSON format when CI needs machine-readable results:
./opa check authz.rego authz_test.rego
./opa test authz.rego authz_test.rego --coverage --format=json
A successful test invocation exits zero. In the validation run, the coverage report showed 100% of the five executable policy lines covered, and the JSON result contained three passing test cases. Store report artifacts only where their access controls are appropriate; production test inputs can include sensitive values even when the policy source is safe to review.
To prove that the denial test is doing useful work, temporarily remove this line from a copy of the policy:
input.change.approved == true
Rerun the same command against the copy. The mutated local policy caused opa test to exit with status 2 and identified test_missing_approval_is_denied as the failed assertion. Restoring the condition made all three tests pass again. This is the operational value of the negative test: it caught a policy that still allowed the approved staging case, but had also begun allowing an unapproved one.
Do not commit the weakened copy. A mutation such as this is a review check, not a deployment artifact. In CI, keep the trusted policy and test files under version control, run the exact OPA version selected for the integration, and fail the job on either opa check or opa test. If a deployment controller queries a different package, decision path, input schema, or OPA bundle than the test, add an integration test in an authorized staging environment. Unit tests establish the decision logic; they do not prove that the controller sends the intended input.
The finished boundary is small but testable. The policy allows the one approved staging request, refuses an unapproved request and a production request, and makes a missing approval condition visible as a failing test. That gives a policy review an executable denial check instead of relying only on the apparent shape of a Rego rule.