Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ tasks:
cache: false

integration:
command: 'go test -tags=integration ./store/postgres'
command: 'go test -count 1 -tags integration ./store/postgres ./testkit/internal/store/postgres ./testkit/internal/authflow'
toolchains: ['go']
inputs:
- '@group(go)'
Expand Down
55 changes: 55 additions & 0 deletions testkit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# authkit testkit

`testkit` is a small pastebin-style web app used to exercise authkit in realistic application code.

The current slice uses authkit's API-token exchange path for paste creation. Reading pastes remains public; creating pastes requires exchanging the startup API token for a short-lived authkit access JWT carried in a temporary app cookie.

## Run

```bash
go run ./testkit/cmd/testkit
```

The server listens on `:8080` by default. Override it with `TESTKIT_ADDR`:

```bash
TESTKIT_ADDR=:8090 go run ./testkit/cmd/testkit
```

Startup prints a fresh development API token:

```text
testkit seed API token: ak_...
```

Use that token on `/login`. The token is shown only at startup and expires after 24 hours.

## Persistence

By default, testkit stores pastes in process memory. Restarting the server clears them.

Set `TESTKIT_DATABASE_URL` to use PostgreSQL paste persistence instead:

```bash
TESTKIT_DATABASE_URL='postgres://testkit:testkit@localhost:5432/testkit?sslmode=disable' \
go run ./testkit/cmd/testkit
```

When `TESTKIT_DATABASE_URL` is set, startup opens a Postgres pool, runs testkit's `testkit_*` paste migrations, runs authkit's Postgres migrations, stores paste data in `testkit_*` tables, and stores authkit principals/API tokens in `authkit_*` tables.

Without `TESTKIT_DATABASE_URL`, both paste data and authkit state are in memory.

## Routes

- `GET /` lists recent pastes.
- `GET /login` renders the API-token login form.
- `POST /auth/token` exchanges an API token and sets the temporary access cookie.
- `POST /logout` clears the temporary access cookie.
- `GET /new` renders the create form for authenticated browsers.
- `POST /pastes` creates a paste for authenticated browsers and redirects to its page.
- `GET /p/{id}` renders a paste.
- `GET /raw/{id}` returns the paste body as `text/plain`.

## Current Scope

The browser cookie is a temporary testkit transport for authkit access JWTs. Ownership, edit/delete flows, refresh tokens, OIDC login, richer session management, and API endpoints are intentionally deferred until this API-token path is proven in the app.
152 changes: 152 additions & 0 deletions testkit/cmd/testkit/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package main

import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/jackc/pgx/v5/pgxpool"

authmemory "github.com/meigma/authkit/store/memory"
authpostgres "github.com/meigma/authkit/store/postgres"
"github.com/meigma/authkit/testkit/internal/authflow"
"github.com/meigma/authkit/testkit/internal/httpui"
"github.com/meigma/authkit/testkit/internal/paste"
testkitmemory "github.com/meigma/authkit/testkit/internal/store/memory"
testkitpostgres "github.com/meigma/authkit/testkit/internal/store/postgres"
)

const (
defaultAddr = ":8080"
addrEnv = "TESTKIT_ADDR"
databaseURLEnv = "TESTKIT_DATABASE_URL"
serverReadHeaderTimeout = 5 * time.Second
serverReadTimeout = 10 * time.Second
serverWriteTimeout = 10 * time.Second
serverIdleTimeout = 60 * time.Second
shutdownTimeout = 5 * time.Second
)

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)

err := run(ctx, os.Stdout)
stop()
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run(ctx context.Context, out io.Writer) error {
addr := os.Getenv(addrEnv)
if addr == "" {
addr = defaultAddr
}

stores, cleanup, err := newStores(ctx)
if err != nil {
return err
}
defer cleanup()

pasteService, err := paste.NewService(stores.pastes)
if err != nil {
return err
}
authRuntime, err := authflow.NewRuntime(ctx, stores.auth)
if err != nil {
return err
}
handler, err := httpui.NewServer(pasteService, authRuntime)
if err != nil {
return err
}

server := &http.Server{
Addr: addr,
Handler: handler,
ReadHeaderTimeout: serverReadHeaderTimeout,
ReadTimeout: serverReadTimeout,
WriteTimeout: serverWriteTimeout,
IdleTimeout: serverIdleTimeout,
}
serverErr := make(chan error, 1)
go func() {
_, _ = fmt.Fprintf(out, "testkit seed API token: %s\n", authRuntime.SeedAPIToken)
_, _ = fmt.Fprintf(out, "testkit listening on http://localhost%s\n", addr)
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
serverErr <- err

return
}
serverErr <- nil
}()

select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("testkit: shutdown server: %w", err)
}

return nil
case err := <-serverErr:
return err
}
}

type stores struct {
pastes paste.Repository
auth authflow.Store
}

func newStores(ctx context.Context) (stores, func(), error) {
databaseURL := os.Getenv(databaseURLEnv)
if databaseURL == "" {
return stores{
pastes: testkitmemory.NewStore(),
auth: authmemory.NewStore(),
}, func() {}, nil
}

pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return stores{}, nil, fmt.Errorf("testkit: open postgres pool: %w", err)
}
if migrateErr := testkitpostgres.Migrate(ctx, pool); migrateErr != nil {
pool.Close()

return stores{}, nil, fmt.Errorf("testkit: migrate postgres: %w", migrateErr)
}
if migrateErr := authpostgres.Migrate(ctx, pool); migrateErr != nil {
pool.Close()

return stores{}, nil, fmt.Errorf("testkit: migrate authkit postgres: %w", migrateErr)
}

pasteStore, err := testkitpostgres.NewStore(pool)
if err != nil {
pool.Close()

return stores{}, nil, err
}
authStore, err := authpostgres.NewStore(pool)
if err != nil {
pool.Close()

return stores{}, nil, err
}

return stores{
pastes: pasteStore,
auth: authStore,
}, pool.Close, nil
}
2 changes: 2 additions & 0 deletions testkit/internal/authflow/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package authflow wires the authkit API-token exchange flow for testkit.
package authflow
Loading