Skip to content

Testing

wardntest runs your real policy in memory, so tests catch typos in permission names exactly as production would.

import "github.com/MogRex-Limited/wardn/wardntest"
w := wardntest.New(t, "../wardn.yaml") // fails the test if the policy is invalid

The 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.Event
w := wardntest.New(t, "../wardn.yaml", func(c *wardn.Config) {
c.OnChange = func(_ context.Context, e wardn.Event) error { events = append(events, e); return nil }
})
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 scope

AsRole 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")
w := wardntest.AllowAll(t, "../wardn.yaml") // everyone may do everything

Permission names are still checked against your policy, so a typo still fails.

clock := wardntest.NewClock(start)
w := wardntest.New(t, "../wardn.yaml", wardntest.WithClock(clock))
clock.Advance(24 * time.Hour)

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 public

If your authentication rejects the audit user, AuditRoutes reports that too. A misconfigured audit can’t pass by accident.

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.