Files
sm-user-plugin/serve_order_test.go
Jakub Zych 4bf44fc6ea fix: logout revokes the session under serve: the jwt blacklist and GORM hooks are wired when the database is published
Boot runs before summer serve publishes the database, so the plugin's GORM
hooks, the Postgres jwt_blacklist store and its sweep now go through
lagoon.OnDatabase, and the jwt guard resolves the published store on every
request. A logged-out token is refused on jwt.auth, /fetch and /refresh.

The jwt guard reads the cookie names from classes.SessionCookieNames().
2026-10-22 22:29:59 +02:00

82 lines
2.6 KiB
Go

package user
import (
"net/http"
"sync/atomic"
"testing"
"git.golem15.com/golem15/sm-user-plugin/classes"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bouncer"
"gorm.io/gorm"
)
// TestBootBeforeDatabase pins logout revocation when Boot runs before the
// database is published, as under summer serve: nothing database-backed is
// installed before lagoon.Publish, the Postgres blacklist store and the GORM
// hooks are installed when it runs, and a token ended by POST /logout is
// refused on jwt.auth through every carrier, at /fetch and at /refresh, with
// its jwt_blacklist row.
func TestBootBeforeDatabase(t *testing.T) {
// The classes hook registrants are process-global and cannot be removed,
// so the test hook is gated and goes inert once this test ends.
var hooked atomic.Pointer[gorm.DB]
var active atomic.Bool
active.Store(true)
t.Cleanup(func() { active.Store(false) })
classes.RegisterHook(func(gdb *gorm.DB) error {
if active.Load() {
hooked.Store(gdb)
}
return nil
})
e := newServeOrderEnv(t, func(app *backpack.App) {
if _, ok := app.Lookup[*gorm.DB](); ok {
t.Fatal("a *gorm.DB is published before lagoon.Publish")
}
if _, ok := app.Lookup[bouncer.BlacklistStore](); ok {
t.Fatal("a bouncer.BlacklistStore is published before lagoon.Publish")
}
if hooked.Load() != nil {
t.Fatal("the GORM hooks ran before lagoon.Publish")
}
})
store, ok := e.app.Lookup[bouncer.BlacklistStore]()
if !ok {
t.Fatal("no bouncer.BlacklistStore published after lagoon.Publish")
}
if _, ok := store.(*bouncer.PostgresBlacklist); !ok {
t.Fatalf("published blacklist store is %T, want *bouncer.PostgresBlacklist", store)
}
if got := hooked.Load(); got != e.gdb {
t.Fatalf("the GORM hooks ran with %p, want the published handle %p", got, e.gdb)
}
token := e.sessionLogin(t, "serve-order")
if code := e.guarded(t, token, ""); code != http.StatusOK {
t.Fatalf("jwt.auth before logout (bearer) = %d", code)
}
for _, name := range classes.SessionCookieNames() {
if code := e.guarded(t, token, name); code != http.StatusOK {
t.Fatalf("jwt.auth before logout (cookie %q) = %d", name, code)
}
}
assertLoggedOut(t, e.call(t, http.MethodPost, userLogoutPath, token, nil))
e.assertSessionEnded(t, token)
jti, _ := parseToken(t, token)["jti"].(string)
if jti == "" {
t.Fatal("session token has no jti")
}
var rows int64
if err := e.gdb.Raw(`SELECT COUNT(*) FROM jwt_blacklist WHERE jti = ?`, jti).Scan(&rows).Error; err != nil {
t.Fatal(err)
}
if rows != 1 {
t.Fatalf("jwt_blacklist rows for %s = %d, want 1", jti, rows)
}
}