- TestMemberTokenScopes walks E-2's acceptance list item by item, plus the per-application catalog and the legacy MintAPIToken fallback; TestMemberTokenScopesBoot (no Docker) the boot refusals - TestBackendLinkAuthTime (C-CR-02), TestBackendLinkShowPost (C-WR-01, C-IN-06), TestAccessRevocation (C-CR-01), TestUserMCPGuardClientIP (C-IN-04), TestAPITokenRevokeEvent (C-IN-02), TestOrganisationLogo (E-1) - README Testing section names them
115 lines
4.3 KiB
Go
115 lines
4.3 KiB
Go
package user
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/sm-user-plugin/controllers"
|
|
"git.golem15.com/golem15/sm-user-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
)
|
|
|
|
// TestBackendLinkAuthTime pins the account link proof of plan 08 (C-CR-02,
|
|
// P-34): confirming a link needs a sign-in at or after the request, read
|
|
// from the session's auth_time, which refresh copies and never renews. A
|
|
// session signed in before the request stays stale through a refresh, a
|
|
// sign-in after the request confirms (also after a refresh), a token
|
|
// without auth_time is stale, and a session token of another user than the
|
|
// authenticated one is 401. Postgres only: under -short nothing runs.
|
|
func TestBackendLinkAuthTime(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
env := newAdminEnv(t)
|
|
login := func(t *testing.T, user *models.User) string {
|
|
t.Helper()
|
|
status, token := env.userLogin(t, user.Email, "correct-horse")
|
|
if status != http.StatusOK || token == "" {
|
|
t.Fatalf("login = %d", status)
|
|
}
|
|
return token
|
|
}
|
|
refresh := func(t *testing.T, token string) string {
|
|
t.Helper()
|
|
rec := env.refresh(t, token)
|
|
var body struct {
|
|
Token string `json:"token"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); rec.Code != http.StatusOK || err != nil || body.Token == "" {
|
|
t.Fatalf("refresh = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
return body.Token
|
|
}
|
|
// signInEarlier waits past the second of the sign-in, so a request
|
|
// created next is strictly later (auth_time has whole seconds).
|
|
signInEarlier := func() { time.Sleep(1100 * time.Millisecond) }
|
|
expect := func(t *testing.T, token, code string, status int, contains string) {
|
|
t.Helper()
|
|
got, body := env.confirmLink(t, token, code)
|
|
if got != status || !strings.Contains(body, contains) {
|
|
t.Fatalf("confirm = %d %s, want %d with %q", got, body, status, contains)
|
|
}
|
|
}
|
|
|
|
t.Run("login, request, refresh, confirm is stale", func(t *testing.T) {
|
|
user := env.seedUser(t, "at-refresh", nil)
|
|
session := login(t, user)
|
|
signInEarlier()
|
|
code := env.startLink(t, env.admin(t))
|
|
expect(t, refresh(t, session), code, http.StatusConflict, `"stale_session"`)
|
|
})
|
|
|
|
t.Run("request, login, confirm links", func(t *testing.T) {
|
|
user := env.seedUser(t, "at-after", nil)
|
|
code := env.startLink(t, env.admin(t))
|
|
expect(t, login(t, user), code, http.StatusOK, `"backend_user"`)
|
|
})
|
|
|
|
t.Run("login, request, login again, confirm links", func(t *testing.T) {
|
|
user := env.seedUser(t, "at-again", nil)
|
|
first := login(t, user)
|
|
signInEarlier()
|
|
code := env.startLink(t, env.admin(t))
|
|
expect(t, first, code, http.StatusConflict, `"stale_session"`)
|
|
expect(t, login(t, user), code, http.StatusOK, `"backend_user"`)
|
|
})
|
|
|
|
t.Run("a hand-made token without auth_time is stale", func(t *testing.T) {
|
|
user := env.seedUser(t, "at-legacy", nil)
|
|
code := env.startLink(t, env.admin(t))
|
|
legacy := craftToken(t, strconv.FormatUint(uint64(user.ID), 10), "at-legacy-"+adminStamp(), time.Now(), time.Now().Add(time.Hour))
|
|
expect(t, legacy, code, http.StatusConflict, `"stale_session"`)
|
|
})
|
|
|
|
t.Run("a refreshed session of a post-request sign-in keeps working", func(t *testing.T) {
|
|
user := env.seedUser(t, "at-carried", nil)
|
|
code := env.startLink(t, env.admin(t))
|
|
session := login(t, user)
|
|
expect(t, refresh(t, refresh(t, session)), code, http.StatusOK, `"backend_user"`)
|
|
})
|
|
|
|
t.Run("another user's session token is 401", func(t *testing.T) {
|
|
user := env.seedUser(t, "at-owner", nil)
|
|
other := env.seedUser(t, "at-other", nil)
|
|
code := env.startLink(t, env.admin(t))
|
|
// jwt.auth authenticated user, but the session token the handler
|
|
// reads again for its sign-in time belongs to other.
|
|
req := httptest.NewRequest(http.MethodPost, "/_user/api/v1/backend-link/confirm", strings.NewReader(`{"code":"`+code+`"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+login(t, other))
|
|
req = req.WithContext(bouncer.WithUser(req.Context(), &bouncer.Principal{ID: user.ID}))
|
|
rec := httptest.NewRecorder()
|
|
controllers.BackendLinkConfirm(env.app)(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("mismatched session token = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
// The code is still usable by its rightful user.
|
|
expect(t, login(t, user), code, http.StatusOK, `"backend_user"`)
|
|
})
|
|
}
|