Summary#
An app is deny-all by default, and a [Test] runs in a secured context as an initially-anonymous principal.
So any test that reads or writes real data must act as some seeded [Principal] row. Two pieces make that
declarative:
principal Name => <selector>;— a top-level declaration that names a selector resolving, at runtime, to a single[Principal]row (e.g.principal Alice => User.Single(u => u.Name == "Alice");). It mirrors apolicy Name => …;, but the body is a single-entity query, not a boolean.[runas(Name)]— an attribute on a[Test]that runs the whole body as that principal.
The runas(<expr>) { … } block form is still there for the case it is uniquely good at: two principals in one
test (Alice creates a row; then as Bob, assert he cannot see it). Attribute = whole test; block = a sub-region;
the inner block wins for its span.
Signature#
principal <Name> => <single-entity selector>; // names a seeded [Principal] row
[Test(<Fixture>)]
[runas(<Name>)] // the whole body runs as <Name>
void <TestName>() { … }Description#
[runas(Name)] binds, never creates. The named selector must resolve to a row the fixture already seeded; a
selector that matches nothing fails the test loudly (it is User.Single(…), not a silent fallback). Naming a
principal that was never declared is a compile error, as is putting [runas] on a function that is not a [Test].
This is deliberate — if [runas] could conjure a principal, a denial test would be fake, quietly passing against a
principal that production would never grant.
Running as a principal rebinds more than the row: the effective role list is resolved for that principal
(so a role-gated allow read when … activates only for a principal actually granted the role), and role-dependent
row filters re-evaluate against the acting user. Data staged during the test settles as that principal at the end
of the run, so an owner-scoped write commits under the owner with no extra ceremony.
A declared principal name is also usable as a value: writing Alice where a value is expected resolves to that
same seeded row (its selector), so new Doc { Owner = Alice } reads naturally — no var alice = User.Single(…)
re-query. An ordinary local, parameter, or entity of the same name always shadows it (the principal name is a
last-resort resolution, never an override).
Use the attribute for the common case — "this whole test runs as one principal" — and reach for the runas(){}
block only when a single test genuinely needs to change principals partway through, or must assert outside the
run-as region (for example, to observe what committed after the block settled).
A principal declaration is authored in a test context alongside the [TestFixture] that seeds its row; the fixture
seeds unsecured (so it can create freely), and the selector gives that row a compile-time name the attribute carries.
Examples#
A single-principal read test — the whole body runs as Alice, who sees only her own owner-scoped rows:
[Principal]
entity User {
[Required, MaxLength(60)] string Name;
security { allow read where Id == user.Id; } // you can read yourself. Nobody enumerates the user table.
}
entity Doc {
[Required] User Owner;
[MaxLength(200)] string Title;
security {
allow create when IsAuthenticated;
allow read where Owner == user;
}
}principal Alice => User.Single(u => u.Name == "Alice");
principal Bob => User.Single(u => u.Name == "Bob");
[TestFixture]
void Seed() {
var alice = new User { Name = "Alice" };
var bob = new User { Name = "Bob" };
var d = new Doc { Owner = alice, Title = "alice-doc" };
}
[Test(Seed)]
[runas(Alice)]
void Alice_sees_only_her_own_rows() {
Assert.NotNull(Doc.FirstOrDefault(d => d.Title == "alice-doc"));
}
// Two principals in one test — the case the attribute alone cannot express. `[runas(Alice)]` is the body's
// default; the inner `runas(Bob) { }` block wins for its span.
[Test(Seed)]
[runas(Alice)]
void Alice_creates_Bob_cannot_see() {
var secret = new Doc { Owner = Alice, Title = "secret" }; // `Alice` as a value — the seeded row
runas(Bob) {
Assert.Null(Doc.FirstOrDefault(d => d.Title == "secret"));
}
}Note that the User table above is locked — nothing may enumerate it, which is how you would really write it. The
selector still finds Alice, because binding who a test acts as is scaffolding, not an app data read: it resolves the
same way the fixture seeds, past the app's own rules. You never have to loosen the user table to test your
security.
The second test is a real proof rather than an illustration: the row is created by one user and genuinely not selected for the other. The rule is inside the query, so there is nothing for Bob to be "hidden" from.
See also#
- Testing (real app, real data, real rules) — the testing guide: who a test acts as, and how you prove a rule
- secure by default (deny-all) — why a test starts deny-all and anonymous
- principal predicates (IsAuthenticated / IsAnonymous) and open reads — the
[Principal]marker and who a request is - Running tests — authoring and running
[Test]/[TestFixture]