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().
82 lines
2.6 KiB
Go
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)
|
|
}
|
|
}
|