Files
summercms/modules/cabana/token_postgres_test.go
Jakub Zych 91830e506f test(16.3-04): pin deny-by-default contract at every layer
Scope match/Allows tables, route classification for every phase09Routes
entry, SPA plaintext lifetime backstops, and Postgres owner lifecycle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 13:17:41 +02:00

132 lines
4.6 KiB
Go

package cabana_test
import (
"bytes"
"context"
"net/http"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/cabana"
)
func TestBackendTokenPostgres(t *testing.T) {
gdb := adminGorm(t)
if err := gdb.AutoMigrate(&tokenDemoWidget{}); err != nil {
t.Fatal(err)
}
h := tokenAdminHandler(t)
user := insertAdmin(t, gdb, "tok-life", "tok-life@example.test", "secret-pass", true, false)
listPath := adminAPI("/acme/demo/widgets")
plain, err := cabana.IssueTestAPIToken(gdb, user.ID, []string{"demo"}, nil)
if err != nil {
t.Fatal(err)
}
if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusOK {
t.Fatalf("initial: %d %s", rec.Code, rec.Body.String())
}
// D-10: deactivate → 401, reactivate → 200.
if err := gdb.Exec(`UPDATE backend_users SET is_activated = false WHERE id = ?`, user.ID).Error; err != nil {
t.Fatal(err)
}
if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusUnauthorized {
t.Fatalf("deactivated: want 401, got %d %s", rec.Code, rec.Body.String())
}
if err := gdb.Exec(`UPDATE backend_users SET is_activated = true WHERE id = ?`, user.ID).Error; err != nil {
t.Fatal(err)
}
if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusOK {
t.Fatalf("reactivated: want 200, got %d %s", rec.Code, rec.Body.String())
}
// Password reset / tokens_valid_after leave the personal API token working.
reset := commandByName(t, cabana.RuntimeCommands(commandApp(t, gdb)), "admin:reset-password")
var buf bytes.Buffer
out := bonfire.NewOutput(nil, &buf, &buf)
if err := reset.Run(context.Background(), flagInput{
args: []string{user.Login},
flags: map[string]string{"password": "new-secret-pass"},
}, out); err != nil {
t.Fatalf("admin:reset-password: %v output=%s", err, buf.String())
}
var afterReset cabana.BackendUser
if err := gdb.First(&afterReset, user.ID).Error; err != nil {
t.Fatal(err)
}
if afterReset.TokensValidAfter == nil {
t.Fatal("reset did not set tokens_valid_after")
}
if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusOK {
t.Fatalf("after password reset: want 200, got %d %s", rec.Code, rec.Body.String())
}
cutoff := time.Now().UTC().Add(time.Hour)
if err := gdb.Model(&cabana.BackendUser{}).Where("id = ?", user.ID).Update("tokens_valid_after", cutoff).Error; err != nil {
t.Fatal(err)
}
if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusOK {
t.Fatalf("after tokens_valid_after cutoff: want 200, got %d %s", rec.Code, rec.Body.String())
}
// Revoked → 401.
now := time.Now().UTC()
if err := gdb.Exec(`UPDATE backend_api_tokens SET revoked_at = ? WHERE backend_user_id = ?`, now, user.ID).Error; err != nil {
t.Fatal(err)
}
if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusUnauthorized {
t.Fatalf("revoked: want 401, got %d %s", rec.Code, rec.Body.String())
}
// Fresh token, expired → 401.
expired := now.Add(-time.Hour)
plain2, err := cabana.IssueTestAPIToken(gdb, user.ID, []string{"demo"}, &expired)
if err != nil {
t.Fatal(err)
}
if rec := getAuth(t, h, listPath, plain2); rec.Code != http.StatusUnauthorized {
t.Fatalf("expired: want 401, got %d %s", rec.Code, rec.Body.String())
}
// Soft-delete owner → 401.
plain3, err := cabana.IssueTestAPIToken(gdb, user.ID, []string{"demo"}, nil)
if err != nil {
t.Fatal(err)
}
if err := gdb.Delete(&user).Error; err != nil {
t.Fatal(err)
}
if rec := getAuth(t, h, listPath, plain3); rec.Code != http.StatusUnauthorized {
t.Fatalf("soft-deleted owner: want 401, got %d %s", rec.Code, rec.Body.String())
}
// last_used_at stamped once within a minute for two requests.
user2 := insertAdmin(t, gdb, "tok-stamp", "tok-stamp@example.test", "secret-pass", true, false)
plain4, err := cabana.IssueTestAPIToken(gdb, user2.ID, []string{"demo"}, nil)
if err != nil {
t.Fatal(err)
}
var tokenID uint
if err := gdb.Raw(`SELECT id FROM backend_api_tokens WHERE backend_user_id = ? ORDER BY id DESC LIMIT 1`, user2.ID).Scan(&tokenID).Error; err != nil || tokenID == 0 {
t.Fatalf("token id: %d err=%v", tokenID, err)
}
if rec := getAuth(t, h, listPath, plain4); rec.Code != http.StatusOK {
t.Fatalf("stamp first: %d %s", rec.Code, rec.Body.String())
}
if rec := getAuth(t, h, listPath, plain4); rec.Code != http.StatusOK {
t.Fatalf("stamp second: %d %s", rec.Code, rec.Body.String())
}
var row cabana.BackendAPIToken
if err := gdb.First(&row, tokenID).Error; err != nil {
t.Fatal(err)
}
if row.LastUsedAt == nil {
t.Fatal("last_used_at not set")
}
// Ensure the stamp path does not write plaintext scopes oddly.
if !row.Scopes.Valid || len(row.Scopes.Get()) == 0 {
t.Fatalf("scopes = %#v", row.Scopes)
}
}