authtest

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

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

View Source
const (
	Password = "correct horse battery staple"
	Wrong    = "incorrect horse battery staple"
)

The two passwords every case uses.

Variables

View Source
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

func RunService(t *testing.T, h Harness)

RunService is the conformance suite. Every implementation of contracts.Service passes it, or it is not one.

func TokenIn

func TokenIn(carrier string) string

TokenIn is the token a set-password link carries, read out of whatever carried it. Both implementations spell the link the same way, which is what makes it a property of the contract rather than of either 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 NewFake

func NewFake(users contracts.Users) *Fake

NewFake returns a fake signing people in from users.

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

func (f *Fake) Forget(_ context.Context, _ db.Tx[db.Tenant], _ string) error

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) Grant

func (f *Fake) Grant(name string, permissions ...string)

Grant is the fake's stand-in for a roles table somebody edited.

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) Logout

func (f *Fake) Logout(_ context.Context, _ db.Tx[db.Tenant], id uuid.UUID) error

Logout mirrors internal.Service.Logout.

func (*Fake) MayAsk

func (f *Fake) MayAsk(ctx context.Context, ip string) bool

MayAsk mirrors internal.Service.MayAsk.

func (*Fake) MayRedeem

func (f *Fake) MayRedeem(ctx context.Context, ip string) bool

MayRedeem mirrors internal.Service.MayRedeem.

func (*Fake) Offer

func (f *Fake) Offer(ctx context.Context, tx db.Tx[db.Tenant], userID uuid.UUID) error

Offer mirrors internal.Service.Offer.

func (*Fake) Open

Open mirrors internal.Service.Open.

func (*Fake) Permissions

func (f *Fake) Permissions(_ context.Context, _ db.Tx[db.Tenant], roles []string) ([]string, error)

Permissions mirrors internal.Service.Permissions.

func (*Fake) Precheck

func (f *Fake) Precheck(ctx context.Context, email, ip string) contracts.Verdict

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

func (f *Fake) Published() []string

Published is the names of the events the fake would have emitted, in order.

func (*Fake) Purge

func (f *Fake) Purge(_ context.Context, _ db.Tx[db.Tenant]) (int64, error)

Purge mirrors internal.Service.Purge over the two maps.

func (*Fake) Reissue

func (f *Fake) Reissue(ctx context.Context, tx db.Tx[db.Tenant], email string) error

Reissue mirrors internal.Service.Reissue: the lookup, in the worker.

func (*Fake) Reset

func (f *Fake) Reset(ctx context.Context, tx db.Tx[db.Tenant], token, password string) error

Reset mirrors internal.Service.Reset: one use, every session ended, one event.

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) Roles

func (f *Fake) Roles(_ context.Context, _ db.Tx[db.Tenant]) ([]*contracts.Role, error)

Roles mirrors internal.Service.Roles.

func (*Fake) SeedRoles

func (f *Fake) SeedRoles(_ context.Context, _ db.Tx[db.System], _ uuid.UUID, operator bool) error

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

func (f *Fake) SessionsOf(user uuid.UUID) int

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 Harness

type Harness func(t *testing.T, run func(Fixture))

Harness builds one Fixture and calls run with it.

type Host

type Host string

Host is a contracts.Hosts that answers one name, which is what a mailed link is built on.

func (Host) PublicHost

func (h Host) PublicHost(context.Context, db.Tx[db.Tenant]) (string, error)

PublicHost is the host, whatever tenant is asking.

type Issuer

type Issuer struct {
	*httptest.Server
	// contains filtered or unexported fields
}

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.

func NewIssuer

func NewIssuer(t interface {
	Fatalf(string, ...any)
	Cleanup(func())
},
) *Issuer

NewIssuer starts one. It is closed when the test ends.

func (*Issuer) Issue

func (i *Issuer) Issue(code, email string, verified bool, audience, nonce string)

Issue registers a code and the claims the token endpoint will return for it. A test names an address and whether the provider says it verified it, because that second answer is what the application refuses on.

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) Send

Send records the message.

func (*Mailbox) Sent

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

Notify records the notice and writes nothing.

func (*Notices) Sent

func (n *Notices) Sent() []notificationcontracts.Notice

Sent is every notice so far, in order.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL