Testing
wardntest runs your real policy in memory, so tests catch typos in permission names exactly as production would.
A Warden for tests
Section titled “A Warden for tests”import "github.com/MogRex-Limited/wardn/wardntest"
w := wardntest.New(t, "../wardn.yaml") // fails the test if the policy is invalidThe path is relative to the test’s package directory, so a test in a subpackage uses "../wardn.yaml".
Options: wardntest.WithClock(clock) for time travel and wardntest.WithStore(store) for a store you’ve prepared. An option is just a func(*wardn.Config), so you can set anything else too, for example to check your audit trail:
var events []wardn.Eventw := wardntest.New(t, "../wardn.yaml", func(c *wardn.Config) { c.OnChange = func(_ context.Context, e wardn.Event) error { events = append(events, e); return nil }})Requests from a user with roles
Section titled “Requests from a user with roles”req := httptest.NewRequest("GET", "/reports", nil)req = wardntest.AsRole(t, w, req, "auditor") // a fresh user holding auditor globally
lagos := wardn.NewScope("branch", "lagos")req = wardntest.AsRoleIn(t, w, req, lagos, "teller") // roles held at a scopeAsRole and AsRoleIn put the user directly in the request’s context, as if your authentication middleware had run. If your router includes that middleware, make sure it only sets a user when a token is present, as in the quick start, so it doesn’t erase the test’s user.
For code that isn’t HTTP, get a user ID instead:
userID := wardntest.NewUser(t, w, lagos, "teller")When authorization isn’t the point
Section titled “When authorization isn’t the point”w := wardntest.AllowAll(t, "../wardn.yaml") // everyone may do everythingPermission names are still checked against your policy, so a typo still fails.
Time travel
Section titled “Time travel”clock := wardntest.NewClock(start)w := wardntest.New(t, "../wardn.yaml", wardntest.WithClock(clock))clock.Advance(24 * time.Hour)Find unprotected routes
Section titled “Find unprotected routes”A route someone forgot to protect is the most common real-world authorization bug. AuditRoutes requests every route as a signed-in user who holds no roles, and fails for any route that doesn’t answer 403:
func TestEveryRouteIsProtected(t *testing.T) { w := wardntest.New(t, "../wardn.yaml") router := newRouter(w)
var routes []wardntest.Route param := regexp.MustCompile(`\{[^}]+\}`) chi.Walk(router.(chi.Routes), func(method, pattern string, _ http.Handler, _ ...func(http.Handler) http.Handler) error { routes = append(routes, wardntest.Route{Method: method, Path: param.ReplaceAllString(pattern, "x")}) return nil })
wardntest.AuditRoutes(t, router, authenticateAs, routes, wardntest.Route{Method: "GET", Path: "/me"}, // open to every signed-in user, on purpose )}
// authenticateAs makes a request look signed in to your real auth middleware.func authenticateAs(r *http.Request, userID string) *http.Request { r.Header.Set("Authorization", "Bearer "+testToken(userID)) return r}A failure reads:
wardntest: DELETE /orgs/x/branches/x/loans/x answered 204 to a user with no roles; protect it with Require, or list it as publicIf your authentication rejects the audit user, AuditRoutes reports that too. A misconfigured audit can’t pass by accident.
Testing a custom store
Section titled “Testing a custom store”If you write your own store, run the conformance suite against it. See Writing a store.
Questions about this page?
Ask below, point out something wrong or unclear, or suggest an improvement. Reading needs no account; to post, sign in with GitHub.