Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: run Playwright in CI #3259

Merged
merged 7 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
123 changes: 111 additions & 12 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
- uses: ory/ci/checkout@master
with:
fetch-depth: 2
- uses: actions/setup-go@v2
- uses: actions/setup-go@v4
with:
go-version: "1.19"
- run: go list -json > go.list
Expand Down Expand Up @@ -168,7 +168,7 @@ jobs:
sudo apt-get install -y moreutils gettext
name: Install tools
- name: Setup Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: "1.19"

Expand Down Expand Up @@ -210,16 +210,115 @@ jobs:
NODE_UI_PATH: node-ui
REACT_UI_PATH: react-ui
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# TODO(hperl): Enable this once the React Native app uses the new SDK
# - name: "Run Playwright tests"
# run: |
# cd test/e2e
# npm run playwright
# env:
# DB: ${{ matrix.database }}
# RN_UI_PATH: react-native-ui
# NODE_UI_PATH: node-ui
# REACT_UI_PATH: react-ui
- if: failure()
uses: actions/upload-artifact@v2
with:
name: logs
path: test/e2e/*.e2e.log

test-e2e-playwright:
name: Run Playwright end-to-end tests
runs-on: ubuntu-latest
needs:
- sdk-generate
services:
postgres:
image: postgres:9.6
env:
POSTGRES_DB: postgres
POSTGRES_PASSWORD: test
POSTGRES_USER: test
ports:
- 5432:5432
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: test
ports:
- 3306:3306
mailslurper:
image: oryd/mailslurper:latest-smtps
ports:
- 4436:4436
- 4437:4437
- 1025:1025
hperl marked this conversation as resolved.
Show resolved Hide resolved
env:
TEST_DATABASE_POSTGRESQL: "postgres://test:test@localhost:5432/postgres?sslmode=disable"
TEST_DATABASE_MYSQL: "mysql://root:test@(localhost:3306)/mysql?parseTime=true&multiStatements=true"
TEST_DATABASE_COCKROACHDB: "cockroach://root@localhost:26257/defaultdb?sslmode=disable"
strategy:
matrix:
database: ["postgres", "cockroach", "sqlite", "mysql"]
steps:
- uses: actions/setup-node@v3
with:
node-version: 16
- run: |
docker create --name cockroach -p 26257:26257 \
cockroachdb/cockroach:v22.2.6 start-single-node --insecure
docker start cockroach
name: Start CockroachDB
- uses: ory/ci/checkout@master
with:
fetch-depth: 2
- run: |
npm ci
cd test/e2e; npm ci
npx playwright install --with-deps
npm i -g expo-cli
name: Install node deps
- run: |
sudo apt-get install -y moreutils gettext
name: Install tools
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.19"
- run: go build -tags sqlite,json1 .

- name: Install selfservice-ui-react-native
uses: actions/checkout@v3
with:
repository: ory/kratos-selfservice-ui-react-native
path: react-native-ui
- run: |
cd react-native-ui
npm install

- name: Install selfservice-ui-node
uses: actions/checkout@v3
with:
repository: ory/kratos-selfservice-ui-node
path: node-ui
- run: |
cd node-ui
npm install

- name: Install selfservice-ui-react-nextjs
uses: actions/checkout@v3
with:
repository: ory/kratos-selfservice-ui-react-nextjs
path: react-ui
- run: |
cd react-ui
npm ci

- run: |
echo 'RN_UI_PATH='"$(realpath react-native-ui)" >> $GITHUB_ENV
echo 'NODE_UI_PATH='"$(realpath node-ui)" >> $GITHUB_ENV
echo 'REACT_UI_PATH='"$(realpath react-ui)" >> $GITHUB_ENV

- name: "Set up environment"
run: test/e2e/run.sh --only-setup
jonas-jonas marked this conversation as resolved.
Show resolved Hide resolved
- name: "Run Playwright tests"
run: |
cd test/e2e
npm run playwright
env:
DB: ${{ matrix.database }}
RN_UI_PATH: react-native-ui
NODE_UI_PATH: node-ui
REACT_UI_PATH: react-ui
- if: failure()
uses: actions/upload-artifact@v2
with:
Expand Down
6 changes: 6 additions & 0 deletions persistence/sql/persister.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ func (p *Persister) CleanupDatabase(ctx context.Context, wait time.Duration, old
}
time.Sleep(wait)

p.r.Logger().Println("Cleaning up expired session token exchangers")
if err := p.DeleteExpiredExchangers(ctx, currentTime, batchSize); err != nil {
return err
}
time.Sleep(wait)

p.r.Logger().Println("Successfully cleaned up the latest batch of the SQL database! " +
"This should be re-run periodically, to be sure that all expired data is purged.")
return nil
Expand Down
16 changes: 16 additions & 0 deletions persistence/sql/persister_cleanup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,19 @@ func TestPersister_Verification_Cleanup(t *testing.T) {
assert.Error(t, p.DeleteExpiredVerificationFlows(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx)))
})
}

func TestPersister_SessionTokenExchange_Cleanup(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
currentTime := time.Now()
ctx := context.Background()

t.Run("case=should not throw error on cleanup session token exchangers", func(t *testing.T) {
assert.Nil(t, p.DeleteExpiredExchangers(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx)))
})

t.Run("case=should throw error on cleanup session token exchangers if DB is closed", func(t *testing.T) {
p.GetConnection(ctx).Close()
assert.Error(t, p.DeleteExpiredExchangers(ctx, currentTime, reg.Config().DatabaseCleanupBatchSize(ctx)))
})
}
19 changes: 19 additions & 0 deletions persistence/sql/persister_sessiontokenexchanger.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package sql
import (
"context"
"fmt"
"time"

"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
Expand Down Expand Up @@ -106,3 +107,21 @@ func (p *Persister) MoveToNewFlow(ctx context.Context, oldFlow, newFlow uuid.UUI

return sqlcon.HandleError(conn.RawQuery(query, newFlow, oldFlow, p.NetworkID(ctx)).Exec())
}

func (p *Persister) DeleteExpiredExchangers(ctx context.Context, at time.Time, limit int) error {
expiredAfter := at.Add(1 * time.Hour)
conn := p.GetConnection(ctx)

//#nosec G201 -- TableName is static
err := conn.RawQuery(fmt.Sprintf(
"DELETE FROM %s WHERE id in (SELECT id FROM (SELECT id FROM %s c WHERE created_at <= ? and nid = ? ORDER BY created_at ASC LIMIT %d ) AS s )",
conn.Dialect.Quote(new(sessiontokenexchange.Exchanger).TableName()),
conn.Dialect.Quote(new(sessiontokenexchange.Exchanger).TableName()),
limit,
),
expiredAfter,
p.NetworkID(ctx),
).Exec()

return sqlcon.HandleError(err)
}
3 changes: 2 additions & 1 deletion selfservice/flow/login/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ func (s *ErrorHandler) WriteFlowError(w http.ResponseWriter, r *http.Request, f
return
}

if _, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.ID); f.Type == flow.TypeAPI && hasCode {
_, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.ID)
if f.Type == flow.TypeAPI && hasCode && group == node.OpenIDConnectGroup {
http.Redirect(w, r, f.ReturnTo, http.StatusSeeOther)
return
}
Expand Down
17 changes: 7 additions & 10 deletions selfservice/flow/login/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ const (
RouteGetFlow = "/self-service/login/flows"

RouteSubmitFlow = "/self-service/login"

RouteExchangeSessionToken = "/self-service/login/exchange-session-token" //nolint:gosec
)

type (
Expand Down Expand Up @@ -135,18 +133,17 @@ func (h *Handler) NewLoginFlow(w http.ResponseWriter, r *http.Request, ft flow.T
return nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse AuthenticationMethod Assurance Level (AAL): %s", cs.ToUnknownCaseErr()))
}

if ft == flow.TypeAPI && r.URL.Query().Get("return_session_token_exchange_code") == "true" {
e, err := h.d.SessionTokenExchangePersister().CreateSessionTokenExchanger(r.Context(), f.ID)
if err != nil {
return nil, nil, errors.WithStack(herodot.ErrInternalServerError.WithWrap(err))
}
f.SessionTokenExchangeCode = e.InitCode
}

// We assume an error means the user has no session
sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r)
if e := new(session.ErrNoActiveSessionFound); errors.As(err, &e) {
// No session exists yet
if ft == flow.TypeAPI && r.URL.Query().Get("return_session_token_exchange_code") == "true" {
e, err := h.d.SessionTokenExchangePersister().CreateSessionTokenExchanger(r.Context(), f.ID)
if err != nil {
return nil, nil, errors.WithStack(herodot.ErrInternalServerError.WithWrap(err))
}
f.SessionTokenExchangeCode = e.InitCode
}

// We can not request an AAL > 1 because we must first verify the first factor.
if f.RequestedAAL > identity.AuthenticatorAssuranceLevel1 {
Expand Down
2 changes: 1 addition & 1 deletion selfservice/flow/login/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func (e *HookExecutor) PostLoginHook(w http.ResponseWriter, r *http.Request, g n

trace.SpanFromContext(r.Context()).AddEvent(events.NewSessionIssued(r.Context(), s.ID, i.ID))

if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID); err != nil {
if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, g); err != nil {
return errors.WithStack(err)
} else if handled {
return nil
Expand Down
2 changes: 1 addition & 1 deletion selfservice/flow/registration/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (s *ErrorHandler) WriteFlowError(
http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(r.Context())).String(), http.StatusFound)
return
}
if _, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.ID); f.Type == flow.TypeAPI && hasCode {
if _, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.ID); group == node.OpenIDConnectGroup && f.Type == flow.TypeAPI && hasCode {
http.Redirect(w, r, f.ReturnTo, http.StatusSeeOther)
return
}
Expand Down
2 changes: 1 addition & 1 deletion selfservice/flow/registration/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque
Debug("Post registration execution hooks completed successfully.")

if a.Type == flow.TypeAPI || x.IsJSONRequest(r) {
if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID); err != nil {
if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, ct.ToUiNodeGroup()); err != nil {
return errors.WithStack(err)
} else if handled {
return nil
Expand Down
13 changes: 9 additions & 4 deletions selfservice/hook/session_issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (

"go.opentelemetry.io/otel/trace"

"github.com/ory/kratos/identity"
"github.com/ory/kratos/ui/node"

"github.com/ory/kratos/x/events"

"github.com/pkg/errors"
Expand Down Expand Up @@ -62,10 +65,12 @@ func (e *SessionIssuer) executePostRegistrationPostPersistHook(w http.ResponseWr
trace.SpanFromContext(r.Context()).AddEvent(events.NewSessionIssued(r.Context(), s.ID, s.IdentityID))

if a.Type == flow.TypeAPI {
if handled, err := e.r.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID); err != nil {
return errors.WithStack(err)
} else if handled {
return nil
if s.AuthenticatedVia(identity.CredentialsTypeOIDC) {
if handled, err := e.r.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, node.OpenIDConnectGroup); err != nil {
return errors.WithStack(err)
} else if handled {
return nil
}
}

a.AddContinueWith(flow.NewContinueWithSetToken(s.Token))
Expand Down
2 changes: 2 additions & 0 deletions selfservice/sessiontokenexchange/persistence.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type (
UpdateSessionOnExchanger(ctx context.Context, flowID uuid.UUID, sessionID uuid.UUID) error
CodeForFlow(ctx context.Context, flowID uuid.UUID) (codes *Codes, found bool, err error)
MoveToNewFlow(ctx context.Context, oldFlow, newFlow uuid.UUID) error

DeleteExpiredExchangers(context.Context, time.Time, int) error
}

PersistenceProvider interface {
Expand Down
3 changes: 2 additions & 1 deletion session/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x/swagger"

"github.com/gofrs/uuid"
Expand Down Expand Up @@ -140,7 +141,7 @@ type Manager interface {

// MaybeRedirectAPICodeFlow for API+Code flows redirects the user to the return_to URL and adds the code query parameter.
// `handled` is true if the request a redirect was written, false otherwise.
MaybeRedirectAPICodeFlow(w http.ResponseWriter, r *http.Request, f flow.Flow, sessionID uuid.UUID) (handled bool, err error)
MaybeRedirectAPICodeFlow(w http.ResponseWriter, r *http.Request, f flow.Flow, sessionID uuid.UUID, uiNode node.UiNodeGroup) (handled bool, err error)
}

type ManagementProvider interface {
Expand Down
7 changes: 6 additions & 1 deletion session/manager_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/sessiontokenexchange"
"github.com/ory/kratos/ui/node"
"github.com/ory/x/otelx"

"github.com/ory/x/randx"
Expand Down Expand Up @@ -324,10 +325,14 @@ func (s *ManagerHTTP) SessionAddAuthenticationMethods(ctx context.Context, sid u
return s.r.SessionPersister().UpsertSession(ctx, sess)
}

func (s *ManagerHTTP) MaybeRedirectAPICodeFlow(w http.ResponseWriter, r *http.Request, f flow.Flow, sessionID uuid.UUID) (handled bool, err error) {
func (s *ManagerHTTP) MaybeRedirectAPICodeFlow(w http.ResponseWriter, r *http.Request, f flow.Flow, sessionID uuid.UUID, uiNode node.UiNodeGroup) (handled bool, err error) {
ctx, span := s.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.ManagerHTTP.MaybeRedirectAPICodeFlow")
defer otelx.End(span, &err)

if uiNode != node.OpenIDConnectGroup {
return false, nil
}
jonas-jonas marked this conversation as resolved.
Show resolved Hide resolved

code, ok, _ := s.r.SessionTokenExchangePersister().CodeForFlow(ctx, f.GetID())
if !ok {
return false, nil
Expand Down
9 changes: 9 additions & 0 deletions session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ func (s *Session) CompletedLoginFor(method identity.CredentialsType, aal identit
s.AMR = append(s.AMR, AuthenticationMethod{Method: method, AAL: aal, CompletedAt: time.Now().UTC()})
}

func (s *Session) AuthenticatedVia(method identity.CredentialsType) bool {
for _, authMethod := range s.AMR {
if authMethod.Method == method {
return true
}
}
return false
}

func (s *Session) SetAuthenticatorAssuranceLevel() {
if len(s.AMR) == 0 {
// No AMR is set
Expand Down
1 change: 1 addition & 0 deletions test/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default defineConfig({
url: "http://localhost:4433/health/ready",
reuseExistingServer: false,
env: { DSN: dbToDsn() },
timeout: 5 * 60 * 1000, // 5 minutes
},
],
})
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/profiles/kratos.base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ selfservice:
default_browser_return_url: http://localhost:4455/
allowed_return_urls:
- http://localhost:4455
- http://localhost:4457/Callback
- exp://example.com/Callback
- https://www.ory.sh/
- https://example.org/
- https://www.example.org/
Expand Down