Documentation
¶
Overview ¶
Package authtest is the conformance suite for contracts.Service, a fake that passes it, and an OpenID Connect issuer a test can sign in against.
Index ¶
- Constants
- Variables
- func RunService(t *testing.T, h Harness)
- func TokenIn(carrier string) string
- type Fake
- func (f *Fake) ChangePassword(ctx context.Context, tx db.Tx[db.Tenant], userID, keep uuid.UUID, ...) error
- func (f *Fake) Forget(_ context.Context, _ db.Tx[db.Tenant], _ string) error
- func (f *Fake) Grant(name string, permissions ...string)
- func (f *Fake) Identify(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, from contracts.Client) (*contracts.Identity, error)
- func (f *Fake) Login(ctx context.Context, tx db.Tx[db.Tenant], email, password string, ...) (*contracts.Session, *contracts.Identity, error)
- func (f *Fake) Logout(_ context.Context, _ db.Tx[db.Tenant], id uuid.UUID) error
- func (f *Fake) MayAsk(ctx context.Context, ip string) bool
- func (f *Fake) MayRedeem(ctx context.Context, ip string) bool
- func (f *Fake) Offer(ctx context.Context, tx db.Tx[db.Tenant], userID uuid.UUID) error
- func (f *Fake) Open(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, from contracts.Client) (*contracts.Session, *contracts.Identity, error)
- func (f *Fake) Permissions(_ context.Context, _ db.Tx[db.Tenant], roles []string) ([]string, error)
- func (f *Fake) Precheck(ctx context.Context, email, ip string) contracts.Verdict
- func (f *Fake) Published() []string
- func (f *Fake) Purge(_ context.Context, _ db.Tx[db.Tenant]) (int64, error)
- func (f *Fake) Reissue(ctx context.Context, tx db.Tx[db.Tenant], email string) error
- func (f *Fake) Reset(ctx context.Context, tx db.Tx[db.Tenant], token, password string) error
- func (f *Fake) RevokeSessions(_ context.Context, _ db.Tx[db.Tenant], userID, except uuid.UUID) error
- func (f *Fake) Roles(_ context.Context, _ db.Tx[db.Tenant]) ([]*contracts.Role, error)
- func (f *Fake) SeedRoles(_ context.Context, _ db.Tx[db.System], _ uuid.UUID, operator bool) error
- func (f *Fake) SessionsOf(user uuid.UUID) int
- func (f *Fake) SetRole(_ context.Context, tx db.Tx[db.Tenant], name string, permissions []string, ...) (*contracts.Role, error)
- type Fixture
- type Harness
- type Host
- type Issuer
- type Mailbox
- type Notices
Constants ¶
const ( Password = "correct horse battery staple" Wrong = "incorrect horse battery staple" )
The two passwords every case uses.
Variables ¶
var Declared = []tenancy.Grant{ {Permission: "widget:read"}, {Permission: "widget:manage"}, {Permission: "fleet:manage", Operator: true}, }
Declared is the permission catalogue the cases hand to SetRole: two ordinary permissions and one operator's. It stands in for what the kernel reads off every manifest, and the names are deliberately not any real module's — what is under test is the rule, not the catalogue.
Functions ¶
func RunService ¶
RunService is the conformance suite. Every implementation of contracts.Service passes it, or it is not one.
Types ¶
type Fake ¶
type Fake struct {
// Users is where the fake finds people. It is exported so that a consumer
// invites somebody through the same interface the real module takes.
Users contracts.Users
// Operator are the permissions the operator's own administrator is granted
// by name, the same list the application hands the real module.
Operator []string
// Notify is where the in-application notice goes. It never carries the
// link, for the reason the real service's does not: a notification is an
// ordinary row and a token in one is a live credential in a table.
Notify contracts.Notifier
// Mailer is where the link itself goes, and the only place it goes. A fake
// with none writes no token and sends nothing, exactly as the real service
// does.
Mailer contracts.Mailer
// contains filtered or unexported fields
}
Fake is contracts.Service over two maps: the same rules, no database, no transaction. A consumer that wants to test what it does for a signed-in caller takes one of these instead of a Postgres.
It keeps the real limiter and hashes with the real argon2id, because both are part of what Login promises rather than of how it stores things. What it cannot be is what row-level security is: there is one tenant here, so "a session from another tenant is invisible" is a claim only the real service can be held to, and internal/service_test.go holds it.
func (*Fake) ChangePassword ¶
func (f *Fake) ChangePassword(ctx context.Context, tx db.Tx[db.Tenant], userID, keep uuid.UUID, current, next string) error
ChangePassword mirrors internal.Service.ChangePassword, current password and session revocation and all: both are what the route promises rather than how it stores anything.
func (*Fake) Forget ¶
Forget mirrors internal.Service.Forget: it publishes and does nothing else, which is what makes the public route cost the same for an address somebody has and one nobody has.
func (*Fake) Identify ¶
func (f *Fake) Identify(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, from contracts.Client) (*contracts.Identity, error)
Identify mirrors internal.Service.Identify, sliding expiry and all.
func (*Fake) Login ¶
func (f *Fake) Login(ctx context.Context, tx db.Tx[db.Tenant], email, password string, from contracts.Client) (*contracts.Session, *contracts.Identity, error)
Login mirrors internal.Service.Login, including the dummy hash on the path where no user was found.
func (*Fake) Open ¶
func (f *Fake) Open(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, from contracts.Client) (*contracts.Session, *contracts.Identity, error)
Open mirrors internal.Service.Open.
func (*Fake) Permissions ¶
Permissions mirrors internal.Service.Permissions.
func (*Fake) Precheck ¶
Precheck mirrors internal.Service.Precheck: the limiter's verdict, from the same limiter the real one keeps, over kit/limit's memory store.
func (*Fake) Published ¶
Published is the names of the events the fake would have emitted, in order.
func (*Fake) RevokeSessions ¶
func (f *Fake) RevokeSessions(_ context.Context, _ db.Tx[db.Tenant], userID, except uuid.UUID) error
RevokeSessions mirrors internal.Service.RevokeSessions.
func (*Fake) SeedRoles ¶
SeedRoles mirrors internal.Service.SeedRoles, Operator and all: the fake is held to the same rule, so a consumer testing against it sees the same refusal the real service gives.
func (*Fake) SessionsOf ¶
SessionsOf is how many sessions this user has, which is what the conformance suite asks after a password change. The real service answers the same question with a count on the table.
func (*Fake) SetRole ¶
func (f *Fake) SetRole(_ context.Context, tx db.Tx[db.Tenant], name string, permissions []string, declared []tenancy.Grant) (*contracts.Role, error)
SetRole mirrors internal.Service.SetRole, both refusals included: a permission nothing declares and an operator permission outside the operator's own tenant are what the route promises to refuse, so the fake refuses them.
type Fixture ¶
type Fixture struct {
Ctx context.Context
Tx db.Tx[db.Tenant]
Service contracts.Service
// User creates a user and returns their id. An empty password makes
// somebody who has been invited and cannot sign in yet.
User func(email, password string, roles ...string) uuid.UUID
// Role grants permissions to a role name in this tenant.
Role func(name string, permissions ...string)
// Published is the names of the events published so far, in order.
Published func() []string
// Sent is every notice the implementation asked to be delivered, in order.
// A case reads one to check what it does not carry.
Sent func() []notificationcontracts.Notice
// Mailed is every message the implementation handed a mail server, in
// order. A case reads the link out of one to follow it, and it is the only
// place it can be read — which is the property, not an inconvenience.
Mailed func() []notificationcontracts.Message
// Sessions reports how many sessions this user has, which is how a case
// says "and the others ended" without knowing how they are stored.
Sessions func(user uuid.UUID) int
}
Fixture is one case's world. The tenant already has the two roles SeedRoles installs, because that is what the tenant module's create hook does and every tenant that exists has been through it.
type Host ¶
type Host string
Host is a contracts.Hosts that answers one name, which is what a mailed link is built on.
type Issuer ¶
Issuer is an OpenID Connect provider a test can sign in against: discovery, a JWKS, and a token endpoint that returns a signed id token for a code the test handed out.
There is no authorization endpoint, because a browser is the thing that would use it: a test follows the redirect itself, reads the state out of it, and calls the callback with a code it registered here. That is the whole round trip the application takes part in.
type Mailbox ¶
type Mailbox struct {
// contains filtered or unexported fields
}
Mailbox is a contracts.Mailer that records instead of sending, which is where a conformance case reads the link an implementation mailed.
It is the only place a case can read it, and that is the property under test: the token is in the message and in no row anywhere. See internal.Service.offer.
func (*Mailbox) Sent ¶
func (m *Mailbox) Sent() []notificationcontracts.Message
Sent is every message so far, in order.
type Notices ¶
type Notices struct {
// contains filtered or unexported fields
}
Notices is a contracts.Notifier that records instead of sending, so a conformance case can read the link an implementation would have mailed.
It is here rather than in each harness because both of them need it and the cases below read what it holds: the suite's claim is that a reset link is issued and works once, and a recorder is the only way to say that without a mail server.
func (*Notices) Notify ¶
func (n *Notices) Notify(_ context.Context, _ db.Tx[db.Tenant], notice notificationcontracts.Notice) (*notificationcontracts.Notification, error)
Notify records the notice and writes nothing.
func (*Notices) Sent ¶
func (n *Notices) Sent() []notificationcontracts.Notice
Sent is every notice so far, in order.