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>
132 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|