From dfa858cd3535ef1559afca096df09b183eebcadb Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:38:42 +0200 Subject: [PATCH] test: login and registration for sms --- courier/courier.go | 36 +---- courier/courier_dispatcher.go | 36 ++++- test/e2e/mock/httptarget/go.mod | 10 ++ test/e2e/mock/httptarget/go.sum | 16 ++ test/e2e/mock/httptarget/main.go | 79 ++++++++++ test/e2e/package-lock.json | 147 ++++++++++++++++-- test/e2e/package.json | 4 +- test/e2e/playwright.config.ts | 6 + test/e2e/playwright/actions/identity.ts | 61 ++++++++ test/e2e/playwright/actions/webhook.ts | 57 +++++++ test/e2e/playwright/fixtures/index.ts | 32 +--- test/e2e/playwright/fixtures/schemas/sms.ts | 35 +++++ test/e2e/playwright/lib/config.ts | 14 ++ .../models/elements/registration.ts | 45 ++++++ .../playwright/tests/desktop/code/sms.spec.ts | 116 ++++++++++++++ 15 files changed, 625 insertions(+), 69 deletions(-) create mode 100644 test/e2e/mock/httptarget/go.mod create mode 100644 test/e2e/mock/httptarget/go.sum create mode 100644 test/e2e/mock/httptarget/main.go create mode 100644 test/e2e/playwright/actions/identity.ts create mode 100644 test/e2e/playwright/actions/webhook.ts create mode 100644 test/e2e/playwright/fixtures/schemas/sms.ts create mode 100644 test/e2e/playwright/lib/config.ts create mode 100644 test/e2e/playwright/models/elements/registration.ts create mode 100644 test/e2e/playwright/tests/desktop/code/sms.spec.ts diff --git a/courier/courier.go b/courier/courier.go index 231b4007060f..cb5acede6aee 100644 --- a/courier/courier.go +++ b/courier/courier.go @@ -47,10 +47,10 @@ type ( } courier struct { - courierChannels map[string]Channel - deps Dependencies - failOnDispatchError bool - backoff backoff.BackOff + deps Dependencies + failOnDispatchError bool + backoff backoff.BackOff + newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error) } ) @@ -58,31 +58,11 @@ func NewCourier(ctx context.Context, deps Dependencies) (Courier, error) { return NewCourierWithCustomTemplates(ctx, deps, NewEmailTemplateFromMessage) } -func NewCourierWithCustomTemplates(ctx context.Context, deps Dependencies, newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error)) (Courier, error) { - cs, err := deps.CourierConfig().CourierChannels(ctx) - if err != nil { - return nil, err - } - channels := make(map[string]Channel, len(cs)) - for _, c := range cs { - switch c.Type { - case "smtp": - ch, err := NewSMTPChannelWithCustomTemplates(deps, c.SMTPConfig, newEmailTemplateFromMessage) - if err != nil { - return nil, err - } - channels[ch.ID()] = ch - case "http": - channels[c.ID] = newHttpChannel(c.ID, c.RequestConfig, deps) - default: - return nil, errors.Errorf("unknown courier channel type: %s", c.Type) - } - } - +func NewCourierWithCustomTemplates(_ context.Context, deps Dependencies, newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error)) (Courier, error) { return &courier{ - deps: deps, - backoff: backoff.NewExponentialBackOff(), - courierChannels: channels, + deps: deps, + backoff: backoff.NewExponentialBackOff(), + newEmailTemplateFromMessage: newEmailTemplateFromMessage, }, nil } diff --git a/courier/courier_dispatcher.go b/courier/courier_dispatcher.go index 47399369c3c3..a0f75954d68e 100644 --- a/courier/courier_dispatcher.go +++ b/courier/courier_dispatcher.go @@ -9,6 +9,33 @@ import ( "github.com/pkg/errors" ) +func (c *courier) channels(ctx context.Context, id string) (Channel, error) { + cs, err := c.deps.CourierConfig().CourierChannels(ctx) + if err != nil { + return nil, err + } + + for _, channel := range cs { + if channel.ID != id { + continue + } + switch channel.Type { + case "smtp": + courierChannel, err := NewSMTPChannelWithCustomTemplates(c.deps, channel.SMTPConfig, c.newEmailTemplateFromMessage) + if err != nil { + return nil, err + } + return courierChannel, nil + case "http": + return newHttpChannel(channel.ID, channel.RequestConfig, c.deps), nil + default: + return nil, errors.Errorf("unknown courier channel type: %s", channel.Type) + } + } + + return nil, errors.Errorf("no courier channels configured") +} + func (c *courier) DispatchMessage(ctx context.Context, msg Message) error { logger := c.deps.Logger(). WithField("message_id", msg.ID). @@ -24,9 +51,9 @@ func (c *courier) DispatchMessage(ctx context.Context, msg Message) error { return err } - channel, ok := c.courierChannels[msg.Channel.String()] - if !ok { - return errors.Errorf("message %s has unknown channel %q", msg.ID.String(), msg.Channel) + channel, err := c.channels(ctx, msg.Channel.String()) + if err != nil { + return err } logger = logger. @@ -80,6 +107,9 @@ func (c *courier) DispatchQueue(ctx context.Context) error { logger. Warnf(`Message was abandoned because it did not deliver after %d attempts`, msg.SendCount) } else if err := c.DispatchMessage(ctx, msg); err != nil { + logger. + WithError(err). + Warn(`Unable to dispatch message.`) if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusFailed, err); err != nil { logger. WithError(err). diff --git a/test/e2e/mock/httptarget/go.mod b/test/e2e/mock/httptarget/go.mod new file mode 100644 index 000000000000..2d66a9ff4f48 --- /dev/null +++ b/test/e2e/mock/httptarget/go.mod @@ -0,0 +1,10 @@ +module github.com/ory/mock + +go 1.22.1 + +require ( + github.com/julienschmidt/httprouter v1.3.0 + github.com/ory/graceful v0.1.3 +) + +require github.com/pkg/errors v0.9.1 // indirect diff --git a/test/e2e/mock/httptarget/go.sum b/test/e2e/mock/httptarget/go.sum new file mode 100644 index 000000000000..e44bf27060c1 --- /dev/null +++ b/test/e2e/mock/httptarget/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/ory/graceful v0.1.3 h1:FaeXcHZh168WzS+bqruqWEw/HgXWLdNv2nJ+fbhxbhc= +github.com/ory/graceful v0.1.3/go.mod h1:4zFz687IAF7oNHHiB586U4iL+/4aV09o/PYLE34t2bA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/test/e2e/mock/httptarget/main.go b/test/e2e/mock/httptarget/main.go new file mode 100644 index 000000000000..c95d0834fb76 --- /dev/null +++ b/test/e2e/mock/httptarget/main.go @@ -0,0 +1,79 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "cmp" + "fmt" + "io" + "log" + "net/http" + "os" + "sync" + + "github.com/julienschmidt/httprouter" + + "github.com/ory/graceful" +) + +var ( + documentsLock sync.RWMutex + documents = make(map[string][]byte) +) + +func main() { + port := cmp.Or(os.Getenv("PORT"), "4471") + server := graceful.WithDefaults(&http.Server{Addr: fmt.Sprintf(":%s", port)}) + register(server) + if err := graceful.Graceful(server.ListenAndServe, server.Shutdown); err != nil { + log.Fatalln(err) + } +} + +func register(server *http.Server) { + router := httprouter.New() + + router.GET("/health", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + _, _ = w.Write([]byte("OK")) + }) + + router.GET("/documents/:id", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.ByName("id") + + documentsLock.RLock() + doc, ok := documents[id] + documentsLock.RUnlock() + + if ok { + _, _ = w.Write(doc) + } else { + w.WriteHeader(http.StatusNotFound) + } + }) + + router.PUT("/documents/:id", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + documentsLock.Lock() + defer documentsLock.Unlock() + id := ps.ByName("id") + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + documents[id] = body + }) + + router.DELETE("/documents/:id", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + documentsLock.Lock() + defer documentsLock.Unlock() + id := ps.ByName("id") + + delete(documents, id) + w.WriteHeader(http.StatusNoContent) + }) + + server.Handler = router +} diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json index ede67e71dde1..7dd8c6416648 100644 --- a/test/e2e/package-lock.json +++ b/test/e2e/package-lock.json @@ -8,7 +8,8 @@ "name": "@ory/kratos-e2e-suite", "version": "0.0.1", "dependencies": { - "@faker-js/faker": "8.4.1", + "@faker-js/faker": "9.0.3", + "@types/promise-retry": "^1.1.6", "async-retry": "1.3.3", "mailhog": "4.16.0", "promise-retry": "^2.0.1" @@ -26,6 +27,7 @@ "got": "11.8.2", "json-schema-to-typescript": "12.0.0", "otplib": "12.0.1", + "phone-number-generator-js": "^1.2.12", "process": "0.11.10", "typescript": "4.7.4", "wait-on": "7.0.1", @@ -99,9 +101,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", - "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.3.tgz", + "integrity": "sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA==", "funding": [ { "type": "opencollective", @@ -110,8 +112,8 @@ ], "license": "MIT", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0", - "npm": ">=6.14.13" + "node": ">=18.0.0", + "npm": ">=9.0.0" } }, "node_modules/@hapi/hoek": { @@ -329,6 +331,15 @@ "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, + "node_modules/@types/promise-retry": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@types/promise-retry/-/promise-retry-1.1.6.tgz", + "integrity": "sha512-EC1+OMXV0PZb0pf+cmyxc43MEP2CDumZe4AfuxWboxxEixztIebknpJPZAX5XlodGF1OY+C1E/RAeNGzxf+bJA==", + "license": "MIT", + "dependencies": { + "@types/retry": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -341,8 +352,7 @@ "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==" }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.1", @@ -356,6 +366,13 @@ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yamljs": { "version": "0.2.31", "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.31.tgz", @@ -780,6 +797,25 @@ "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", "dev": true }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, + "node_modules/class-validator/node_modules/libphonenumber-js": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.9.tgz", + "integrity": "sha512-Zs5wf5HaWzW2/inlupe2tstl0I/Tbqo7lH20ZLr6Is58u7Dz2n+gRFGNlj9/gWxFvNfp9+YyDsiegjNhdixB9A==", + "dev": true, + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -1991,6 +2027,13 @@ "node": "> 0.8" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.30", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.30.tgz", + "integrity": "sha512-PLGc+xfrQrkya/YK2/5X+bPpxRmyJBHM+xxz9krUdSgk4Vs2ZwxX5/Ow0lv3r9PDlDtNWb4u+it8MY5rZ0IyGw==", + "dev": true, + "license": "MIT" + }, "node_modules/listr2": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", @@ -2390,6 +2433,18 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, + "node_modules/phone-number-generator-js": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/phone-number-generator-js/-/phone-number-generator-js-1.2.12.tgz", + "integrity": "sha512-AtJpQjHFlXqD2ZMZLUlzrNKNTwwyn9gFASeTgfcGqdWUUHsddThKkCbsJ7VyDyj7C2Xo0oce/XOARH8eElas6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "class-validator": "0.14.1", + "libphonenumber-js": "1.10.30", + "lodash": "4.17.21" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -2471,6 +2526,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -2989,6 +3045,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -3198,9 +3264,9 @@ } }, "@faker-js/faker": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", - "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==" + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.3.tgz", + "integrity": "sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA==" }, "@hapi/hoek": { "version": "9.3.0", @@ -3400,6 +3466,14 @@ "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, + "@types/promise-retry": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@types/promise-retry/-/promise-retry-1.1.6.tgz", + "integrity": "sha512-EC1+OMXV0PZb0pf+cmyxc43MEP2CDumZe4AfuxWboxxEixztIebknpJPZAX5XlodGF1OY+C1E/RAeNGzxf+bJA==", + "requires": { + "@types/retry": "*" + } + }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -3412,8 +3486,7 @@ "@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==" }, "@types/sinonjs__fake-timers": { "version": "8.1.1", @@ -3427,6 +3500,12 @@ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", "dev": true }, + "@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "dev": true + }, "@types/yamljs": { "version": "0.2.31", "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.31.tgz", @@ -3744,6 +3823,25 @@ "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", "dev": true }, + "class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dev": true, + "requires": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + }, + "dependencies": { + "libphonenumber-js": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.9.tgz", + "integrity": "sha512-Zs5wf5HaWzW2/inlupe2tstl0I/Tbqo7lH20ZLr6Is58u7Dz2n+gRFGNlj9/gWxFvNfp9+YyDsiegjNhdixB9A==", + "dev": true + } + } + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -4671,6 +4769,12 @@ "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", "dev": true }, + "libphonenumber-js": { + "version": "1.10.30", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.30.tgz", + "integrity": "sha512-PLGc+xfrQrkya/YK2/5X+bPpxRmyJBHM+xxz9krUdSgk4Vs2ZwxX5/Ow0lv3r9PDlDtNWb4u+it8MY5rZ0IyGw==", + "dev": true + }, "listr2": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", @@ -4971,6 +5075,17 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, + "phone-number-generator-js": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/phone-number-generator-js/-/phone-number-generator-js-1.2.12.tgz", + "integrity": "sha512-AtJpQjHFlXqD2ZMZLUlzrNKNTwwyn9gFASeTgfcGqdWUUHsddThKkCbsJ7VyDyj7C2Xo0oce/XOARH8eElas6A==", + "dev": true, + "requires": { + "class-validator": "0.14.1", + "libphonenumber-js": "1.10.30", + "lodash": "4.17.21" + } + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -5409,6 +5524,12 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, + "validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "dev": true + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/test/e2e/package.json b/test/e2e/package.json index 05b04db6880a..a33ac330bf00 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -11,7 +11,8 @@ "wait-on": "wait-on" }, "dependencies": { - "@faker-js/faker": "8.4.1", + "@faker-js/faker": "9.0.3", + "@types/promise-retry": "^1.1.6", "async-retry": "1.3.3", "mailhog": "4.16.0", "promise-retry": "^2.0.1" @@ -29,6 +30,7 @@ "got": "11.8.2", "json-schema-to-typescript": "12.0.0", "otplib": "12.0.1", + "phone-number-generator-js": "^1.2.12", "process": "0.11.10", "typescript": "4.7.4", "wait-on": "7.0.1", diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index a3f81c73060d..2ace64395520 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -65,6 +65,12 @@ export default defineConfig({ reuseExistingServer: false, url: "http://localhost:8025/", }, + { + command: "go run test/e2e/mock/httptarget/main.go", + cwd: "../..", + reuseExistingServer: false, + url: "http://localhost:4471/health", + }, ], }) diff --git a/test/e2e/playwright/actions/identity.ts b/test/e2e/playwright/actions/identity.ts new file mode 100644 index 000000000000..f05bc2f68a3e --- /dev/null +++ b/test/e2e/playwright/actions/identity.ts @@ -0,0 +1,61 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { faker } from "@faker-js/faker" +import { APIRequestContext } from "@playwright/test" +import { CreateIdentityBody } from "@ory/kratos-client" +import { generatePhoneNumber, CountryNames } from "phone-number-generator-js" +import { expect } from "../fixtures" + +export async function createIdentity( + request: APIRequestContext, + data: Partial, +) { + const resp = await request.post("http://localhost:4434/admin/identities", { + data, + }) + expect(resp.status()).toBe(201) + return await resp.json() +} + +export async function createIdentityWithPhoneNumber( + request: APIRequestContext, +) { + const phone = generatePhoneNumber({ + countryName: CountryNames.Germany, + withoutCountryCode: false, + }) + return { + identity: await createIdentity(request, { + schema_id: "sms", + traits: { + phone, + }, + }), + phone, + } +} + +export async function createIdentityWithPassword(request: APIRequestContext) { + const email = faker.internet.email({ provider: "ory.sh" }) + const password = faker.internet.password() + return { + identity: await createIdentity(request, { + schema_id: "email", + traits: { + email, + website: faker.internet.url(), + }, + + credentials: { + password: { + config: { + password, + }, + }, + }, + }), + email, + password, + } +} diff --git a/test/e2e/playwright/actions/webhook.ts b/test/e2e/playwright/actions/webhook.ts new file mode 100644 index 000000000000..82b2511d5d90 --- /dev/null +++ b/test/e2e/playwright/actions/webhook.ts @@ -0,0 +1,57 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { request } from "@playwright/test" +import retry from "promise-retry" +import { retryOptions } from "../lib/config" + +export const WEBHOOK_TARGET = "http://127.0.0.1:4471" + +const baseUrl = WEBHOOK_TARGET + +/** + * Fetches a documented (hopefully) created by web hook + * + * @param key + */ +export async function fetchDocument(key: string) { + const r = await request.newContext() + + return retry(async (retry) => { + const res = await r.get(documentUrl(key)) + if (res.status() !== 200) { + const body = await res.text() + const message = `Expected response code 200 but received ${res.status()}: ${body}` + return retry(message) + } + return await res.json() + }, retryOptions) +} + +/** + * Fetches a documented (hopefully) created by web hook + * + * @param key + */ +export async function deleteDocument(key: string) { + const r = await request.newContext() + + return retry(async (retry) => { + const res = await r.delete(documentUrl(key)) + if (res.status() !== 204) { + const body = await res.text() + const message = `Expected response code 204 but received ${res.status()}: ${body}` + return retry(message) + } + return + }, retryOptions) +} + +/** + * Returns the URL for a specific document + * + * @param key + */ +export function documentUrl(key: string) { + return `${baseUrl}/documents/${key}` +} diff --git a/test/e2e/playwright/fixtures/index.ts b/test/e2e/playwright/fixtures/index.ts index b915dd4d937f..7f227ac7b5ee 100644 --- a/test/e2e/playwright/fixtures/index.ts +++ b/test/e2e/playwright/fixtures/index.ts @@ -1,14 +1,13 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { faker } from "@faker-js/faker" import { Identity } from "@ory/kratos-client" import { + APIRequestContext, CDPSession, - test as base, expect as baseExpect, - APIRequestContext, Page, + test as base, } from "@playwright/test" import { writeFile } from "fs/promises" import { merge } from "lodash" @@ -19,6 +18,7 @@ import { SessionWithResponse } from "../types" import { retryOptions } from "../lib/request" import promiseRetry from "promise-retry" import { Protocol } from "playwright-core/types/protocol" +import { createIdentityWithPassword } from "../actions/identity" // from https://stackoverflow.com/questions/61132262/typescript-deep-partial type DeepPartial = T extends object @@ -104,31 +104,15 @@ export const test = base.extend({ await pageCDPSession.send("WebAuthn.disable") }, identity: async ({ request }, use, i) => { - const email = faker.internet.email({ provider: "ory.sh" }) - const password = faker.internet.password() - const resp = await request.post("http://localhost:4434/admin/identities", { - data: { - schema_id: "email", - traits: { - email, - website: faker.internet.url(), - }, - - credentials: { - password: { - config: { - password, - }, - }, - }, - }, - }) - const oryIdentity = await resp.json() + const { + identity: oryIdentity, + password, + email, + } = await createIdentityWithPassword(request) i.attach("identity", { body: JSON.stringify(oryIdentity, null, 2), contentType: "application/json", }) - expect(resp.status()).toBe(201) await use({ oryIdentity, email, diff --git a/test/e2e/playwright/fixtures/schemas/sms.ts b/test/e2e/playwright/fixtures/schemas/sms.ts new file mode 100644 index 000000000000..0e5186633dfb --- /dev/null +++ b/test/e2e/playwright/fixtures/schemas/sms.ts @@ -0,0 +1,35 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export default { + $id: "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + $schema: "http://json-schema.org/draft-07/schema#", + title: "Person", + type: "object", + properties: { + traits: { + type: "object", + properties: { + phone: { + type: "string", + format: "tel", + title: "Your Phone Number", + minLength: 3, + "ory.sh/kratos": { + credentials: { + code: { + identifier: true, + via: "sms", + }, + }, + verification: { + via: "sms", + }, + }, + }, + }, + required: ["phone"], + additionalProperties: false, + }, + }, +} diff --git a/test/e2e/playwright/lib/config.ts b/test/e2e/playwright/lib/config.ts new file mode 100644 index 000000000000..4584d51f746e --- /dev/null +++ b/test/e2e/playwright/lib/config.ts @@ -0,0 +1,14 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import type { OperationOptions } from "retry" + +export type RetryOptions = OperationOptions + +export const retryOptions: RetryOptions = { + retries: 20, + factor: 1, + maxTimeout: 500, + minTimeout: 250, + randomize: false, +} diff --git a/test/e2e/playwright/models/elements/registration.ts b/test/e2e/playwright/models/elements/registration.ts new file mode 100644 index 000000000000..029903f14e49 --- /dev/null +++ b/test/e2e/playwright/models/elements/registration.ts @@ -0,0 +1,45 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { expect, Page } from "@playwright/test" +import { createInputLocator, InputLocator } from "../../selectors/input" +import { OryKratosConfiguration } from "../../../shared/config" + +export class RegistrationPage { + public identifier: InputLocator + + constructor(readonly page: Page, readonly config: OryKratosConfiguration) { + this.identifier = createInputLocator(page, "identifier") + } + + async open() { + await Promise.all([ + this.page.goto(this.config.selfservice.flows.registration.ui_url), + this.isReady(), + this.page.waitForURL((url) => + url + .toString() + .includes(this.config.selfservice.flows.registration.ui_url), + ), + ]) + await this.isReady() + } + + inputField(name: string) { + return this.page.locator(`input[name="${name}"]`) + } + + submitField(name: string) { + return this.page.locator(`[type="submit"][name="method"][value="${name}"]`) + } + + async isReady() { + await expect(this.inputField("csrf_token").nth(0)).toBeHidden() + } + + async triggerRegistrationWithCode(identifier: string) { + await this.inputField("traits.phone").fill(identifier) + await this.submitField("profile").click() + await this.submitField("code").click() + } +} diff --git a/test/e2e/playwright/tests/desktop/code/sms.spec.ts b/test/e2e/playwright/tests/desktop/code/sms.spec.ts new file mode 100644 index 000000000000..f52176d6644c --- /dev/null +++ b/test/e2e/playwright/tests/desktop/code/sms.spec.ts @@ -0,0 +1,116 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { test } from "../../../fixtures" +import { toConfig } from "../../../lib/helper" +import smsSchema from "../../../fixtures/schemas/sms" +import { LoginPage } from "../../../models/elements/login" +import { hasSession } from "../../../actions/session" +import { createIdentityWithPhoneNumber } from "../../../actions/identity" +import { + deleteDocument, + documentUrl, + fetchDocument, +} from "../../../actions/webhook" +import { RegistrationPage } from "../../../models/elements/registration" +import { CountryNames, generatePhoneNumber } from "phone-number-generator-js" + +const documentId = "doc-" + Math.random().toString(36).substring(7) + +test.describe.only("account enumeration protection off", () => { + test.use({ + configOverride: { + security: { + account_enumeration: { + enabled: false, + }, + }, + selfservice: { + flows: { + login: { + style: "unified", + }, + registration: { + after: { + code: { + hooks: [ + { + hook: "session", + }, + ], + }, + }, + }, + }, + methods: { + code: { + passwordless_enabled: true, + }, + password: { + enabled: false, + }, + }, + }, + courier: { + channels: [ + { + id: "sms", + type: "http", + request_config: { + body: "base64://ZnVuY3Rpb24oY3R4KSB7DQpjdHg6IGN0eCwNCn0=", + method: "PUT", + url: documentUrl(documentId), + }, + }, + ], + }, + identity: { + default_schema_id: "sms", + schemas: [ + { + id: "sms", + url: + "base64://" + + Buffer.from(JSON.stringify(smsSchema), "ascii").toString( + "base64", + ), + }, + ], + }, + }, + }) + + test.afterEach(async () => { + await deleteDocument(documentId) + }) + + test("login succeeds", async ({ page, config, kratosPublicURL }) => { + const identity = await createIdentityWithPhoneNumber(page.request) + + const login = new LoginPage(page, config) + await login.open() + await login.triggerLoginWithCode(identity.phone) + + const result = await fetchDocument(documentId) + await login.codeInput.input.fill(result.ctx.template_data.login_code) + await login.codeSubmit.getByText("Continue").click() + await hasSession(page.request, kratosPublicURL) + }) + + test("registration succeeds", async ({ page, config, kratosPublicURL }) => { + const phone = generatePhoneNumber({ + countryName: CountryNames.Germany, + withoutCountryCode: false, + }) + + const registration = new RegistrationPage(page, config) + await registration.open() + await registration.triggerRegistrationWithCode(phone) + + const result = await fetchDocument(documentId) + const code = result.ctx.template_data.registration_code + await registration.inputField("code").fill(code) + await registration.submitField("code").getByText("Continue").click() + await hasSession(page.request, kratosPublicURL) + }) +})