Files
upstream-ory-kratos/continuity/manager_test.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

336 lines
12 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package continuity_test
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/herodot"
"github.com/ory/kratos/continuity"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/x"
"github.com/ory/x/configx"
"github.com/ory/x/ioutilx"
"github.com/ory/x/logrusx"
)
type persisterTestCase struct {
ro []continuity.ManagerOption
wo []continuity.ManagerOption
expected *persisterTestPayload
expectedErr error
}
type persisterTestPayload struct {
Foo string `json:"foo"`
}
func TestManager(t *testing.T) {
_, reg := pkg.NewFastRegistryWithMocks(t,
configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://../test/stub/identity/empty.schema.json")),
)
i := identity.NewIdentity("")
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i))
newServer := func(t *testing.T, p *continuity.Manager, tc *persisterTestCase) *httptest.Server {
writer := herodot.NewJSONWriter(logrusx.New("", ""))
router := http.NewServeMux()
router.HandleFunc("PUT /{name}", func(w http.ResponseWriter, r *http.Request) {
store := continuity.NewCookieReferenceStore(reg.ContinuityCookieManager(r.Context()))
if _, err := p.Pause(r.Context(), w, r, r.PathValue("name"), store, tc.ro...); err != nil {
writer.WriteError(w, r, err)
return
}
w.WriteHeader(http.StatusNoContent)
})
router.HandleFunc("POST /{name}", func(w http.ResponseWriter, r *http.Request) {
store := continuity.NewCookieReferenceStore(reg.ContinuityCookieManager(r.Context()))
if _, err := p.Pause(r.Context(), w, r, r.PathValue("name"), store, tc.ro...); err != nil {
writer.WriteError(w, r, err)
return
}
c, err := p.Continue(r.Context(), w, r, r.PathValue("name"), store, tc.wo...)
if err != nil {
writer.WriteError(w, r, err)
return
}
writer.Write(w, r, c)
})
router.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) {
store := continuity.NewCookieReferenceStore(reg.ContinuityCookieManager(r.Context()))
c, err := p.Continue(r.Context(), w, r, r.PathValue("name"), store, tc.ro...)
if err != nil {
writer.WriteError(w, r, err)
return
}
writer.Write(w, r, c)
})
router.HandleFunc("DELETE /{name}", func(w http.ResponseWriter, r *http.Request) {
store := continuity.NewCookieReferenceStore(reg.ContinuityCookieManager(r.Context()))
err := p.Abort(r.Context(), w, r, r.PathValue("name"), store)
if err != nil {
writer.WriteError(w, r, err)
return
}
w.WriteHeader(http.StatusNoContent)
})
ts := httptest.NewServer(router)
t.Cleanup(ts.Close)
return ts
}
newClient := func() *http.Client {
return &http.Client{Jar: testhelpers.EasyCookieJar(t, nil), Transport: testhelpers.NewTestTransport(t)}
}
p := reg.ContinuityManager()
cl := newClient()
t.Run("case=continue cookie resets when signature is invalid", func(t *testing.T) {
ts := newServer(t, p, new(persisterTestCase))
href := ts.URL + "/" + x.NewUUID().String()
res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusNoContent, res.StatusCode)
req := testhelpers.NewTestHTTPRequest(t, "GET", href, nil)
require.Len(t, res.Cookies(), 1)
for _, c := range res.Cookies() {
// Change something in the string
c.Value = strings.Replace(c.Value, "a", "b", 1)
req.AddCookie(c)
}
res, err = testhelpers.NewTestClient(t).Do(req)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, res.Body.Close()) })
require.Equal(t, http.StatusBadRequest, res.StatusCode)
body := ioutilx.MustReadAll(res.Body)
assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable().ReasonField)
require.Len(t, res.Cookies(), 1, "continuing the flow with a broken cookie should instruct the browser to forget it")
assert.EqualValues(t, res.Cookies()[0].Name, continuity.CookieName)
})
t.Run("case=can deal with duplicate cookies", func(t *testing.T) {
tc := &persisterTestCase{expected: &persisterTestPayload{"bar"}}
ts := newServer(t, p, tc)
href := ts.URL + "/" + x.NewUUID().String()
res, err := testhelpers.NewTestClient(t).Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusNoContent, res.StatusCode)
// We change the key to another one
href = ts.URL + "/" + x.NewUUID().String()
req := testhelpers.NewTestHTTPRequest(t, "GET", href, nil)
require.Len(t, res.Cookies(), 1)
for _, c := range res.Cookies() {
req.AddCookie(c)
}
tc.ro = []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"})}
res, err = testhelpers.NewTestClient(t).Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusNoContent, res.StatusCode)
require.Len(t, res.Cookies(), 1)
for _, c := range res.Cookies() {
req.AddCookie(c)
}
res, err = testhelpers.NewTestClient(t).Do(req)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, res.Body.Close()) })
require.Len(t, res.Cookies(), 1, "continuing the flow with a broken cookie should instruct the browser to forget it")
assert.EqualValues(t, res.Cookies()[0].Name, continuity.CookieName)
var b bytes.Buffer
require.NoError(t, json.NewEncoder(&b).Encode(tc.expected))
body := ioutilx.MustReadAll(res.Body)
assert.JSONEq(t, b.String(), gjson.GetBytes(body, "payload").Raw, "%s", body)
assert.Contains(t, href, gjson.GetBytes(body, "name").String(), "%s", body)
})
t.Run("case=pause and use session with expiry", func(t *testing.T) {
cl := newClient()
tc := &persisterTestCase{
ro: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"}), continuity.WithExpireInsteadOfDelete(time.Minute)},
wo: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{}), continuity.WithExpireInsteadOfDelete(time.Minute)},
}
ts := newServer(t, p, tc)
genid := func() string {
return ts.URL + "/" + x.NewUUID().String()
}
href := genid()
res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusNoContent, res.StatusCode)
res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusOK, res.StatusCode)
res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusOK, res.StatusCode)
tc.ro = []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"}), continuity.WithExpireInsteadOfDelete(-time.Minute)}
tc.wo = []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{""}), continuity.WithExpireInsteadOfDelete(-time.Minute)}
res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusOK, res.StatusCode)
res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil))
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, res.StatusCode)
body := ioutilx.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable().ReasonField)
})
for k, tc := range []persisterTestCase{
{},
{
ro: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"})},
wo: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{})},
expected: &persisterTestPayload{"bar"},
},
{
ro: []continuity.ManagerOption{continuity.WithIdentity(i)},
wo: []continuity.ManagerOption{continuity.WithIdentity(i)},
},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
cl := newClient()
ts := newServer(t, p, &tc)
genid := func() string {
return ts.URL + "/" + x.NewUUID().String()
}
t.Run("case=resume non-existing session", func(t *testing.T) {
href := genid()
res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, res.Body.Close()) })
body := ioutilx.MustReadAll(res.Body)
require.Equal(t, http.StatusBadRequest, res.StatusCode)
assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable().ReasonField)
})
t.Run("case=pause and resume session", func(t *testing.T) {
href := genid()
res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusNoContent, res.StatusCode)
res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, res.Body.Close()) })
body := ioutilx.MustReadAll(res.Body)
if tc.expectedErr != nil {
require.Equal(t, http.StatusGone, res.StatusCode, "%s", body)
return
}
require.Equal(t, http.StatusOK, res.StatusCode, "%s", body)
var b bytes.Buffer
require.NoError(t, json.NewEncoder(&b).Encode(tc.expected))
assert.JSONEq(t, b.String(), gjson.GetBytes(body, "payload").Raw, "%s", body)
assert.Contains(t, href, gjson.GetBytes(body, "name").String(), "%s", body)
})
t.Run("case=pause and retry session", func(t *testing.T) {
href := genid()
res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusNoContent, res.StatusCode)
res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, res.Body.Close()) })
res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil))
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, res.StatusCode)
body := ioutilx.MustReadAll(res.Body)
t.Cleanup(func() { require.NoError(t, res.Body.Close()) })
assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable().ReasonField)
})
t.Run("case=pause and resume session in the same request", func(t *testing.T) {
href := genid()
res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "POST", href, nil))
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.StatusCode)
t.Cleanup(func() { require.NoError(t, res.Body.Close()) })
var b bytes.Buffer
require.NoError(t, json.NewEncoder(&b).Encode(tc.expected))
body := ioutilx.MustReadAll(res.Body)
assert.JSONEq(t, b.String(), gjson.GetBytes(body, "payload").Raw, "%s", body)
assert.Contains(t, href, gjson.GetBytes(body, "name").String(), "%s", body)
})
t.Run("case=pause, abort, and continue session with failure", func(t *testing.T) {
href := genid()
res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil))
require.NoError(t, err)
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusNoContent, res.StatusCode)
res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "DELETE", href, nil))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, res.Body.Close()) })
require.Equal(t, http.StatusNoContent, res.StatusCode)
res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, res.Body.Close()) })
require.Equal(t, http.StatusBadRequest, res.StatusCode)
body := ioutilx.MustReadAll(res.Body)
assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable().ReasonField)
})
})
}
}