Files
sm-user-plugin/backend_link_auth_time_test.go
Jakub Zych e3c164fdf0 test: named tests for the account link proof, revocation, member token scopes and the organisation logo
- 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
2026-10-23 18:36:39 +02:00

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"`)
})
}