Sign in as a test user
The first test you wrote carried [SignedInAs], and every journey in the sample carries it. This lesson reads what it declares, who carries it to the application, and what the trace keeps about it.
- Declare the identity a test acts as with [SignedInAs].
- Read the auth entity and the member role the sample derives from it.
- Tell the in-process case from the published one, and what claim values never reach the trace.
- Add and remove an integration (lesson 3).
- The sample cloned. Reading the archive alone also works.
The scenario
Every Northstar journey names the user it acts as. [SignedInAs] with no arguments is the tenant's owner; a name and a role make the test a viewer who is refused. The declaration is the test's identity, not a credential.
The sample's application has its own member store, so it uses the identity to decide which member the journey acts as. An application without its own authentication can opt in to the shipped test user instead. This lesson reads both sides and the trace they leave.
The declaration
The sample's first journey shows both forms:
1[Application(NorthstarTargets.Api)]2[NorthstarMember(PlanIds.Growth)]3public sealed class ProjectsJourney4{5[ProtoTest]6[SignedInAs("viewer", MemberRoles.Viewer)]7public async Task AViewerCannotCreateProjects()8{9using var response = await Proto.Context.Rest()10.Body(new CreateProjectRequest("viewer-atlas"))11.PostAsync("/api/v1/projects");1213response.Should.HaveHttpStatus(HttpStatusCode.Forbidden);14}15}
- Start the context
[ProtoTest] wraps this test in its own execution context; the identity belongs to that context alone.
- Name the user and the role
The name comes first and roles follow; with no arguments the built-in name test-user is used. Here the viewer role is what the application refuses.
- The application decides
The 403 comes from the application, which resolved the viewer and applied its own authorization.
samples/Northstar.ProtoTest/ProjectsJourney.cs. Claims use the same attribute: Claims = new[] { "tenant=northstar" }.Everything in the declaration is constant attribute data: names and roles, not secrets. One identity per test; declaring it twice replaces, not merges.
What the declaration becomes
The attribute's whole before step publishes the identity:
1public override Task BeforeTestAsync(ProtoExecutionContext context)2{3context.SignIn(new ProtoTestUser(4Name,5Roles,6[.. Claims.Select(ParseClaim)]));7return Task.CompletedTask;8}
- One call publishes the identity
SignIn sets the type the context resolves and records the auth:user entity, so the next test starts with none.
- Name and roles
A bare [SignedInAs] uses the built-in name test-user.
- Claim values stay types in the trace
The values travel in the header; the trace records their types only.
src/ProtoTest.Http/Authentication/SignedInAsAttribute.cs. REST, GraphQL and gRPC requests carry the identity through the same auth lifecycle.The sample turns that identity into a member. [NorthstarMember] composes a tenant and the sample's authenticator, and the member resolution reads the role:
1var user = context.SignedInUser();2var organization = context.Resolve<NorthstarOrganizationContext>();3var role = user.Roles.Count > 0 ? user.Roles[0] : MemberRoles.Owner;4var member = role == MemberRoles.Owner5? new NorthstarMemberContext("owner", organization.OwnerEmail, role, organization.OwnerToken)6: await InviteAsync(context, role);
- Read the identity
SignedInUser() throws a message naming both ways to declare one when the test has none.
- The role picks the member
No declared role means the tenant owner; the first role invites a member with exactly that role.
- The member is the credential
The sample sends the member token as the credential; its application resolves that, not the shipped test-user header.
samples/Northstar.ProtoTest/NorthstarMember.cs. The invited member's email carries the role and the test id, so parallel tests never share one.The app side
The sample keeps its own authentication. An application that should treat the test user as its own principal opts in to the shipped handler instead:
builder.AddApplication("Api", app => app
.AddAspNetCoreServer<Program>(webHost => webHost.AddTestUserAuthentication())
.AddRest(rest => rest.AddClient("Api")));
The handler decodes the ProtoTest-User header into a ClaimsPrincipal with the name, the roles and the custom claims. It becomes the default authentication scheme, so the application's own [Authorize] and role checks decide exactly as in production. The sample leaves it out on purpose: its subject is the application's own authentication, and the handler replaces the default scheme.
What the trace records
The first journey's archive is the in-process case:
| Entry | Reading |
|---|---|
Before · NorthstarTenantAttribute, 139.9 ms | the tenant is provisioned first, at Order -200 |
Before · SignedInAsAttribute, 1.7 ms | the declaration runs next, at Order -100, and its event reads Signed in as test-user |
Apply · TestUserAuthenticator, 1.2 ms | the shipped transport applies for the in-process server |
Apply · NorthstarAuthenticator, 1.0 ms | the sample's own authenticator rides along on the same request |
auth entity auth:user: name test-user, roles empty, claim types empty, application Northstar, transport in-process | the identity the trace keeps |
Rest and GraphQL auth entities: source class, count 2, types ProtoTest.Http.SignedInAsAttribute, Northstar.ProtoTest.NorthstarAuthenticator | the declaration composes with the app's own authenticator instead of replacing it |
Claim values never appear. The entity records auth.claim_types, a list of types, and a gRPC call's prototest-user metadata is redacted in the trace as well.
The published case
The shipped transport needs an application the run hosts in-process. Point the sample at an address that hosts nothing, the dead one from the Level 0 environment drill, and add a test that declares the identity and nothing else:
1[Application(NorthstarTargets.Api)]2public sealed class PublishedAliceProbe3{4[ProtoTest]5[SignedInAs("alice", "admin", Claims = ["tenant=northstar"])]6public async Task TheIdentityIsStillRecorded()7{8using var response = await Proto.Context.Rest()9.Body(new CreateProjectRequest("alice-atlas"))10.PostAsync("/api/v1/projects");1112response.Should.HaveHttpStatus(HttpStatusCode.Created);13}14}
- Select the published application
The same [Application] the sample journeys carry. Nothing in this test hosts it.
- Declare the identity
This is the declaration the lesson is about. The request behind it is ordinary.
samples/Northstar.ProtoTest/. Leave [NorthstarMember] off: it provisions a tenant first, that call needs a live address, and it would fail before this declaration runs.Run it by name against the dead address:
$env:ProtoTest__TargetUrl = "http://127.0.0.1:5099"
dotnet test samples/Northstar.ProtoTest --filter "FullyQualifiedName~PublishedAliceProbe"
Remove-Item Env:ProtoTest__TargetUrl
The request fails, because nothing answers at the address. The identity entity in the trace is still written, and in that run it reads:
auth.user: alice,auth.roles: admin,auth.claim_types: tenant;auth.transport: inert, with the reason: the application is not hosted in-process, so register it withAddAspNetCoreServerand add the app-side authentication withwebHost.AddTestUserAuthentication();- an
auth.user.inertevent with outcomeskipped.
The literal tenant=northstar appears nowhere in that trace. The name, the role and the claim type do.
That is the limit of the shipped transport: a published application holds no test user unless the suite declares its own [Auth<T>] authenticator that reads context.SignedInUser(). The authentication reference shows that shape and the full precedence rules.
Checkpoint
A test declares [SignedInAs("alice", "admin", Claims = ["tenant=northstar"])]. Which parts reach the trace, and which value does not?
auth:user entity in l1-first-journey.prototrace, or run the published case below with a test that carries only [Application], [ProtoTest] and [SignedInAs]. The reason the entity records is in src/ProtoTest.Http/Authentication/SignedInAsAttribute.cs.tenant are recorded: auth.user: alice, auth.roles: admin, auth.claim_types: tenant. The claim value northstar is nowhere; claim values stay in the header and never reach the trace.What you learned
- A bare [SignedInAs] signs in as the built-in test-user; a name, roles and claims describe the identity further.
- The identity is per-test state; the trace shows the name, roles and claim types, never claim values.
- The shipped transport needs an in-process application. Against a published one it is inert and records why.