diff --git a/.gitignore b/.gitignore index 6eaa44db9..e31e48a42 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ npm-debug.log* # Coverage reports coverage/ -src/.baseDir.ts .vscode/ *.swp diff --git a/cmd/authelia-scripts/cmd_bootstrap.go b/cmd/authelia-scripts/cmd_bootstrap.go index 89561ccf3..d29e6006b 100644 --- a/cmd/authelia-scripts/cmd_bootstrap.go +++ b/cmd/authelia-scripts/cmd_bootstrap.go @@ -55,10 +55,6 @@ func runCommand(cmd string, args ...string) { } } -func installNpmPackages() { - runCommand("npm", "ci") -} - func checkCommandExist(cmd string) { fmt.Print("Checking if '" + cmd + "' command is installed...") command := exec.Command("bash", "-c", "command -v "+cmd) @@ -213,9 +209,6 @@ func Bootstrap(cobraCmd *cobra.Command, args []string) { log.Fatal("GOPATH is not set") } - bootstrapPrintln("Installing NPM packages for development...") - installNpmPackages() - bootstrapPrintln("Building development Docker images...") buildHelperDockerImages() diff --git a/cmd/authelia/main.go b/cmd/authelia/main.go index 1d48f470e..a5ceb3b96 100644 --- a/cmd/authelia/main.go +++ b/cmd/authelia/main.go @@ -16,6 +16,7 @@ import ( "github.com/clems4ever/authelia/internal/server" "github.com/clems4ever/authelia/internal/session" "github.com/clems4ever/authelia/internal/storage" + "github.com/clems4ever/authelia/internal/utils" "github.com/sirupsen/logrus" ) @@ -51,12 +52,15 @@ func main() { switch config.LogsLevel { case "info": + logging.Logger().Info("Logging severity set to info") logging.SetLevel(logrus.InfoLevel) break case "debug": + logging.Logger().Info("Logging severity set to debug") logging.SetLevel(logrus.DebugLevel) break case "trace": + logging.Logger().Info("Logging severity set to trace") logging.SetLevel(logrus.TraceLevel) } @@ -90,9 +94,10 @@ func main() { log.Fatalf("Unrecognized notifier") } + clock := utils.RealClock{} authorizer := authorization.NewAuthorizer(*config.AccessControl) sessionProvider := session.NewProvider(config.Session) - regulator := regulation.NewRegulator(config.Regulation, storageProvider) + regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock) providers := middlewares.Providers{ Authorizer: authorizer, diff --git a/example/compose/authelia/docker-compose.backend.yml b/example/compose/authelia/docker-compose.backend.yml index cddc87e4c..101f27276 100644 --- a/example/compose/authelia/docker-compose.backend.yml +++ b/example/compose/authelia/docker-compose.backend.yml @@ -1,4 +1,4 @@ -version: '3' +version: "3" services: authelia-backend: build: @@ -12,7 +12,6 @@ services: - "${GOPATH}:/go" - "/tmp/authelia:/tmp/authelia" environment: - - SUITE_PATH=${SUITE_PATH} - ENVIRONMENT=dev networks: authelianet: diff --git a/example/compose/authelia/resources/entrypoint.sh b/example/compose/authelia/resources/entrypoint.sh index f5e577380..54469e105 100755 --- a/example/compose/authelia/resources/entrypoint.sh +++ b/example/compose/authelia/resources/entrypoint.sh @@ -4,4 +4,7 @@ set -x go get github.com/cespare/reflex +mkdir -p /var/lib/authelia +mkdir -p /etc/authelia + reflex -c /resources/reflex.conf \ No newline at end of file diff --git a/example/compose/authelia/resources/run.sh b/example/compose/authelia/resources/run.sh index f6fab20e0..e20ed017d 100755 --- a/example/compose/authelia/resources/run.sh +++ b/example/compose/authelia/resources/run.sh @@ -27,7 +27,4 @@ retry() { # Build the binary go build -o /tmp/authelia/authelia-tmp cmd/authelia/main.go -# Run the temporary binary -cd $SUITE_PATH - -retry 3 /tmp/authelia/authelia-tmp -config ${SUITE_PATH}/configuration.yml \ No newline at end of file +retry 3 /tmp/authelia/authelia-tmp -config /etc/authelia/configuration.yml \ No newline at end of file diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go index 3458ce761..0f9c3ed72 100644 --- a/internal/handlers/handler_firstfactor.go +++ b/internal/handlers/handler_firstfactor.go @@ -35,13 +35,15 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { userPasswordOk, err := ctx.Providers.UserProvider.CheckUserPassword(bodyJSON.Username, bodyJSON.Password) if err != nil { + ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username) + ctx.Providers.Regulator.Mark(bodyJSON.Username, false) + ctx.Error(fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) return } ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username) - // Mark the authentication attempt and whether it was successful. - err = ctx.Providers.Regulator.Mark(bodyJSON.Username, userPasswordOk) + err = ctx.Providers.Regulator.Mark(bodyJSON.Username, false) if err != nil { ctx.Error(fmt.Errorf("Unable to mark authentication: %s", err), authenticationFailedMessage) diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go index 11e42b550..241c362a0 100644 --- a/internal/handlers/handler_firstfactor_test.go +++ b/internal/handlers/handler_firstfactor_test.go @@ -3,8 +3,10 @@ package handlers import ( "fmt" "testing" + "time" "github.com/clems4ever/authelia/internal/mocks" + "github.com/clems4ever/authelia/internal/models" "github.com/clems4ever/authelia/internal/authentication" "github.com/golang/mock/gomock" @@ -70,6 +72,32 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() { s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") } +func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCredentials() { + t, _ := time.Parse("2006-Jan-02", "2013-Feb-03") + s.mock.Clock.Set(t) + + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). + Return(false, fmt.Errorf("Invalid credentials")) + + s.mock.StorageProviderMock. + EXPECT(). + AppendAuthenticationLog(gomock.Eq(models.AuthenticationAttempt{ + Username: "test", + Successful: false, + Time: t, + })) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "keepMeLoggedIn": true + }`) + + FirstFactorPost(s.mock.Ctx) +} + func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() { s.mock.UserProviderMock. EXPECT(). diff --git a/internal/mocks/mock_authelia_ctx.go b/internal/mocks/mock_authelia_ctx.go index e8f75a261..148794d79 100644 --- a/internal/mocks/mock_authelia_ctx.go +++ b/internal/mocks/mock_authelia_ctx.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "testing" + "time" "github.com/clems4ever/authelia/internal/regulation" "github.com/clems4ever/authelia/internal/storage" @@ -32,11 +33,34 @@ type MockAutheliaCtx struct { NotifierMock *MockNotifier UserSession *session.UserSession + + Clock TestingClock +} + +// TestingClock implementation of clock for tests +type TestingClock struct { + now time.Time +} + +// Now return the stored clock +func (dc *TestingClock) Now() time.Time { + return dc.now +} + +// After return a channel receiving the time after duration has elapsed +func (dc *TestingClock) After(d time.Duration) <-chan time.Time { + return time.After(d) +} + +// Set set the time of the clock +func (dc *TestingClock) Set(now time.Time) { + dc.now = now } // NewMockAutheliaCtx create an instance of AutheliaCtx mock func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx { mockAuthelia := new(MockAutheliaCtx) + mockAuthelia.Clock = TestingClock{} configuration := schema.Configuration{ AccessControl: new(schema.AccessControlConfiguration), @@ -75,7 +99,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx { providers.SessionProvider = session.NewProvider( configuration.Session) - providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider) + providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider, &mockAuthelia.Clock) request := &fasthttp.RequestCtx{} // Set a cookie to identify this client throughout the test @@ -94,6 +118,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx { // Close close the mock func (m *MockAutheliaCtx) Close() { m.Hook.Reset() + m.Ctrl.Finish() } // Assert200KO assert an error response from the service. diff --git a/internal/regulation/regulator.go b/internal/regulation/regulator.go index 15450a156..1997008cd 100644 --- a/internal/regulation/regulator.go +++ b/internal/regulation/regulator.go @@ -7,11 +7,13 @@ import ( "github.com/clems4ever/authelia/internal/configuration/schema" "github.com/clems4ever/authelia/internal/models" "github.com/clems4ever/authelia/internal/storage" + "github.com/clems4ever/authelia/internal/utils" ) // NewRegulator create a regulator instance. -func NewRegulator(configuration *schema.RegulationConfiguration, provider storage.Provider) *Regulator { +func NewRegulator(configuration *schema.RegulationConfiguration, provider storage.Provider, clock utils.Clock) *Regulator { regulator := &Regulator{storageProvider: provider} + regulator.clock = clock if configuration != nil { if configuration.FindTime > configuration.BanTime { panic(fmt.Errorf("find_time cannot be greater than ban_time")) @@ -30,7 +32,7 @@ func (r *Regulator) Mark(username string, successful bool) error { return r.storageProvider.AppendAuthenticationLog(models.AuthenticationAttempt{ Username: username, Successful: successful, - Time: time.Now(), + Time: r.clock.Now(), }) } @@ -42,7 +44,7 @@ func (r *Regulator) Regulate(username string) (time.Time, error) { if !r.enabled { return time.Time{}, nil } - now := time.Now() + now := r.clock.Now() // TODO(c.michaud): make sure FindTime < BanTime. attempts, err := r.storageProvider.LoadLatestAuthenticationLogs(username, now.Add(-r.banTime)) diff --git a/internal/regulation/types.go b/internal/regulation/types.go index 96c0443bf..02add27e8 100644 --- a/internal/regulation/types.go +++ b/internal/regulation/types.go @@ -4,6 +4,7 @@ import ( "time" "github.com/clems4ever/authelia/internal/storage" + "github.com/clems4ever/authelia/internal/utils" ) // Regulator an authentication regulator preventing attackers to brute force the service. @@ -18,4 +19,6 @@ type Regulator struct { banTime time.Duration storageProvider storage.Provider + + clock utils.Clock } diff --git a/internal/suites/BypassAll/configuration.yml b/internal/suites/BypassAll/configuration.yml index 2ac9b8ce3..e5e8af4c2 100644 --- a/internal/suites/BypassAll/configuration.yml +++ b/internal/suites/BypassAll/configuration.yml @@ -10,7 +10,7 @@ jwt_secret: unsecure_secret authentication_backend: file: - path: users.yml + path: /var/lib/authelia/users.yml session: secret: unsecure_session_secret @@ -31,14 +31,13 @@ duo_api: access_control: default_policy: bypass rules: - - domain: 'public.example.com' - policy: bypass - - domain: 'secure.example.com' - policy: two_factor + - domain: "public.example.com" + policy: bypass + - domain: "secure.example.com" + policy: two_factor notifier: smtp: host: smtp port: 1025 sender: admin@example.com - diff --git a/internal/suites/BypassAll/docker-compose.yml b/internal/suites/BypassAll/docker-compose.yml new file mode 100644 index 000000000..9aa75ee5b --- /dev/null +++ b/internal/suites/BypassAll/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/BypassAll/configuration.yml:/etc/authelia/configuration.yml:ro" + - "./internal/suites/BypassAll/users.yml:/var/lib/authelia/users.yml" diff --git a/internal/suites/DuoPush/configuration.yml b/internal/suites/DuoPush/configuration.yml index 7f7b31e5f..19762b4a4 100644 --- a/internal/suites/DuoPush/configuration.yml +++ b/internal/suites/DuoPush/configuration.yml @@ -4,13 +4,13 @@ port: 9091 -logs_level: debug +logs_level: trace jwt_secret: very_important_secret authentication_backend: file: - path: users.yml + path: /var/lib/authelia/users.yml session: secret: unsecure_session_secret @@ -38,7 +38,7 @@ duo_api: # Access Control # -# Access control is a set of rules you can use to restrict user access to certain +# Access control is a set of rules you can use to restrict user access to certain # resources. access_control: # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`. @@ -54,39 +54,38 @@ access_control: - domain: secure.example.com policy: two_factor - - domain: '*.example.com' + - domain: "*.example.com" subject: "group:admins" policy: two_factor - domain: dev.example.com resources: - - '^/users/john/.*$' + - "^/users/john/.*$" subject: "user:john" policy: two_factor - domain: dev.example.com resources: - - '^/users/harry/.*$' + - "^/users/harry/.*$" subject: "user:harry" policy: two_factor - - domain: '*.mail.example.com' + - domain: "*.mail.example.com" subject: "user:bob" policy: two_factor - domain: dev.example.com resources: - - '^/users/bob/.*$' + - "^/users/bob/.*$" subject: "user:bob" policy: two_factor - # Configuration of the authentication regulation mechanism. -regulation: +regulation: # Set it to 0 to disable max_retries. max_retries: 3 - # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window. + # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window. find_time: 300 # The length of time before a banned user can login again. @@ -98,4 +97,3 @@ notifier: host: smtp port: 1025 sender: admin@example.com - diff --git a/internal/suites/DuoPush/docker-compose.yml b/internal/suites/DuoPush/docker-compose.yml new file mode 100644 index 000000000..8a0bae4b6 --- /dev/null +++ b/internal/suites/DuoPush/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/DuoPush/configuration.yml:/etc/authelia/configuration.yml:ro" + - "./internal/suites/DuoPush/users.yml:/var/lib/authelia/users.yml" diff --git a/internal/suites/HighAvailability/docker-compose.yml b/internal/suites/HighAvailability/docker-compose.yml new file mode 100644 index 000000000..6f07db2ef --- /dev/null +++ b/internal/suites/HighAvailability/docker-compose.yml @@ -0,0 +1,5 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/HighAvailability/configuration.yml:/etc/authelia/configuration.yml:ro" diff --git a/internal/suites/LDAP/docker-compose.yml b/internal/suites/LDAP/docker-compose.yml new file mode 100644 index 000000000..9ecfdcad6 --- /dev/null +++ b/internal/suites/LDAP/docker-compose.yml @@ -0,0 +1,5 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/LDAP/configuration.yml:/etc/authelia/configuration.yml:ro" diff --git a/internal/suites/Mariadb/configuration.yml b/internal/suites/Mariadb/configuration.yml index c07ea901d..b74f1d791 100644 --- a/internal/suites/Mariadb/configuration.yml +++ b/internal/suites/Mariadb/configuration.yml @@ -12,7 +12,7 @@ jwt_secret: very_important_secret authentication_backend: file: - path: users.yml + path: /var/lib/authelia/users.yml session: secret: unsecure_session_secret diff --git a/internal/suites/Mariadb/docker-compose.yml b/internal/suites/Mariadb/docker-compose.yml new file mode 100644 index 000000000..a1b9f0724 --- /dev/null +++ b/internal/suites/Mariadb/docker-compose.yml @@ -0,0 +1,5 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/Mariadb/configuration.yml:/etc/authelia/configuration.yml:ro" diff --git a/internal/suites/NetworkACL/docker-compose.yml b/internal/suites/NetworkACL/docker-compose.yml new file mode 100644 index 000000000..893814404 --- /dev/null +++ b/internal/suites/NetworkACL/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/NetworkACL/configuration.yml:/etc/authelia/configuration.yml:ro" + - "./internal/suites/NetworkACL/users.yml:/var/lib/authelia/users.yml" diff --git a/internal/suites/Postgres/configuration.yml b/internal/suites/Postgres/configuration.yml index 39da51523..9cfdc2445 100644 --- a/internal/suites/Postgres/configuration.yml +++ b/internal/suites/Postgres/configuration.yml @@ -12,7 +12,7 @@ jwt_secret: very_important_secret authentication_backend: file: - path: users.yml + path: /var/lib/authelia/users.yml session: secret: unsecure_session_secret diff --git a/internal/suites/Postgres/docker-compose.yml b/internal/suites/Postgres/docker-compose.yml new file mode 100644 index 000000000..e68752ef6 --- /dev/null +++ b/internal/suites/Postgres/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/Postgres/configuration.yml:/etc/authelia/configuration.yml:ro" + - "./internal/suites/Postgres/users.yml:/var/lib/authelia/users.yml" diff --git a/test/suites/README.md b/internal/suites/README.md similarity index 100% rename from test/suites/README.md rename to internal/suites/README.md diff --git a/internal/suites/ShortTimeouts/configuration.yml b/internal/suites/ShortTimeouts/configuration.yml index f1255188a..1f0e36de7 100644 --- a/internal/suites/ShortTimeouts/configuration.yml +++ b/internal/suites/ShortTimeouts/configuration.yml @@ -12,7 +12,7 @@ default_redirection_url: https://home.example.com:8080/ authentication_backend: file: - path: users.yml + path: /var/lib/authelia/users.yml session: secret: unsecure_session_secret diff --git a/internal/suites/ShortTimeouts/docker-compose.yml b/internal/suites/ShortTimeouts/docker-compose.yml new file mode 100644 index 000000000..e2ae7e19a --- /dev/null +++ b/internal/suites/ShortTimeouts/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/ShortTimeouts/configuration.yml:/etc/authelia/configuration.yml:ro" + - "./internal/suites/ShortTimeouts/users.yml:/var/lib/authelia/users.yml" diff --git a/internal/suites/Standalone/configuration.yml b/internal/suites/Standalone/configuration.yml index 0e64db1ce..e3f1cb636 100644 --- a/internal/suites/Standalone/configuration.yml +++ b/internal/suites/Standalone/configuration.yml @@ -12,7 +12,7 @@ jwt_secret: very_important_secret authentication_backend: file: - path: users.yml + path: /var/lib/authelia/users.yml session: secret: unsecure_session_secret @@ -22,7 +22,7 @@ session: storage: local: - path: db.sqlite3 + path: /tmp/authelia/db.sqlite3 totp: issuer: example.com @@ -40,37 +40,36 @@ access_control: - domain: secure.example.com policy: two_factor - - domain: '*.example.com' + - domain: "*.example.com" subject: "group:admins" policy: two_factor - domain: dev.example.com resources: - - '^/users/john/.*$' + - "^/users/john/.*$" subject: "user:john" policy: two_factor - domain: dev.example.com resources: - - '^/users/harry/.*$' + - "^/users/harry/.*$" subject: "user:harry" policy: two_factor - - domain: '*.mail.example.com' + - domain: "*.mail.example.com" subject: "user:bob" policy: two_factor - domain: dev.example.com resources: - - '^/users/bob/.*$' + - "^/users/bob/.*$" subject: "user:bob" policy: two_factor - -regulation: +regulation: # Set it to 0 to disable max_retries. max_retries: 3 - # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window. + # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window. find_time: 300 # The length of time before a banned user can login again. ban_time: 900 @@ -80,4 +79,3 @@ notifier: host: smtp port: 1025 sender: admin@example.com - diff --git a/internal/suites/Standalone/docker-compose.yml b/internal/suites/Standalone/docker-compose.yml new file mode 100644 index 000000000..9231bb69f --- /dev/null +++ b/internal/suites/Standalone/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/Standalone/configuration.yml:/etc/authelia/configuration.yml:ro" + - "./internal/suites/Standalone/users.yml:/var/lib/authelia/users.yml" diff --git a/internal/suites/Standalone/users.yml b/internal/suites/Standalone/users.yml index f42c0e42f..e14c81d09 100644 --- a/internal/suites/Standalone/users.yml +++ b/internal/suites/Standalone/users.yml @@ -1,28 +1,20 @@ -############################################################### -# Users Database # -############################################################### - -# This file can be used if you do not have an LDAP set up. - users: - john: - password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" - email: john.doe@authelia.com - groups: - - admins - - dev - - harry: - password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" - email: harry.potter@authelia.com - groups: [] - bob: - password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + password: '{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/' email: bob.dylan@authelia.com groups: - - dev - + - dev + harry: + password: '{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/' + email: harry.potter@authelia.com + groups: [] james: - password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" - email: james.dean@authelia.com \ No newline at end of file + password: '{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/' + email: james.dean@authelia.com + groups: [] + john: + password: '{CRYPT}$6$rounds=50000$LnfgDsc2WD8F2qNf$0gcCt8jlqAGZRv2ee3mCFsfAr1P4N7kESWEf36Xtw6OjkhAcQuGVOBHXp0lFuZbppa7YlgHk3VD28aSQu9U9S1' + email: john.doe@authelia.com + groups: + - admins + - dev diff --git a/internal/suites/Traefik/configuration.yml b/internal/suites/Traefik/configuration.yml index 586c57662..135267b9b 100644 --- a/internal/suites/Traefik/configuration.yml +++ b/internal/suites/Traefik/configuration.yml @@ -10,7 +10,7 @@ jwt_secret: unsecure_secret authentication_backend: file: - path: users.yml + path: /var/lib/authelia/users.yml session: secret: unsecure_session_secret diff --git a/internal/suites/Traefik/docker-compose.yml b/internal/suites/Traefik/docker-compose.yml new file mode 100644 index 000000000..4c29932b9 --- /dev/null +++ b/internal/suites/Traefik/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" +services: + authelia-backend: + volumes: + - "./internal/suites/Traefik/configuration.yml:/etc/authelia/configuration.yml:ro" + - "./internal/suites/Traefik/users.yml:/var/lib/authelia/users.yml" diff --git a/internal/suites/action_2fa_methods.go b/internal/suites/action_2fa_methods.go new file mode 100644 index 000000000..17ccaca20 --- /dev/null +++ b/internal/suites/action_2fa_methods.go @@ -0,0 +1,12 @@ +package suites + +import ( + "context" + "fmt" + "testing" +) + +func (wds *WebDriverSession) doChangeMethod(ctx context.Context, t *testing.T, method string) { + wds.WaitElementLocatedByID(ctx, t, "methods-button").Click() + wds.WaitElementLocatedByID(ctx, t, fmt.Sprintf("%s-option", method)).Click() +} diff --git a/internal/suites/action_http.go b/internal/suites/action_http.go index e96faabca..7b0e33a2e 100644 --- a/internal/suites/action_http.go +++ b/internal/suites/action_http.go @@ -1,24 +1,21 @@ package suites import ( - "crypto/tls" "io/ioutil" "net/http" + "testing" "github.com/stretchr/testify/assert" ) -func doHTTPGetQuery(s *SeleniumSuite, url string) []byte { - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - client := &http.Client{Transport: tr} +func doHTTPGetQuery(t *testing.T, url string) []byte { + client := NewHTTPClient() req, err := http.NewRequest("GET", url, nil) - assert.NoError(s.T(), err) + assert.NoError(t, err) req.Header.Add("Accept", "application/json") resp, err := client.Do(req) - assert.NoError(s.T(), err) + assert.NoError(t, err) defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) diff --git a/internal/suites/action_login.go b/internal/suites/action_login.go index 870119cc8..a568bb00d 100644 --- a/internal/suites/action_login.go +++ b/internal/suites/action_login.go @@ -2,46 +2,57 @@ package suites import ( "context" + "testing" "github.com/stretchr/testify/assert" ) -func doFillLoginPageAndClick(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool) { - usernameElement := WaitElementLocatedByID(ctx, s, "username") +func (wds *WebDriverSession) doFillLoginPageAndClick(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool) { + usernameElement := wds.WaitElementLocatedByID(ctx, t, "username-textfield") err := usernameElement.SendKeys(username) - assert.NoError(s.T(), err) + assert.NoError(t, err) - passwordElement := WaitElementLocatedByID(ctx, s, "password") + passwordElement := wds.WaitElementLocatedByID(ctx, t, "password-textfield") err = passwordElement.SendKeys(password) - assert.NoError(s.T(), err) + assert.NoError(t, err) if keepMeLoggedIn { - keepMeLoggedInElement := WaitElementLocatedByID(ctx, s, "remember-checkbox") + keepMeLoggedInElement := wds.WaitElementLocatedByID(ctx, t, "remember-checkbox") err = keepMeLoggedInElement.Click() - assert.NoError(s.T(), err) + assert.NoError(t, err) } - buttonElement := WaitElementLocatedByTagName(ctx, s, "button") + buttonElement := wds.WaitElementLocatedByID(ctx, t, "sign-in-button") err = buttonElement.Click() - assert.NoError(s.T(), err) + assert.NoError(t, err) } -func doLoginOneFactor(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool, targetURL string) { - doVisitLoginPage(ctx, s, targetURL) - doFillLoginPageAndClick(ctx, s, username, password, keepMeLoggedIn) +// Login 1FA +func (wds *WebDriverSession) doLoginOneFactor(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool, targetURL string) { + wds.doVisitLoginPage(ctx, t, targetURL) + wds.doFillLoginPageAndClick(ctx, t, username, password, keepMeLoggedIn) } -func doLoginTwoFactor(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool, otpSecret, targetURL string) { - doLoginOneFactor(ctx, s, username, password, keepMeLoggedIn, targetURL) - verifyIsSecondFactorPage(ctx, s) - doValidateTOTP(ctx, s, otpSecret) +// Login 1FA and 2FA subsequently (must already be registered) +func (wds *WebDriverSession) doLoginTwoFactor(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool, otpSecret, targetURL string) { + wds.doLoginOneFactor(ctx, t, username, password, keepMeLoggedIn, targetURL) + wds.verifyIsSecondFactorPage(ctx, t) + wds.doValidateTOTP(ctx, t, otpSecret) } -func doLoginAndRegisterTOTP(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool) string { - doLoginOneFactor(ctx, s, username, password, keepMeLoggedIn, "") - secret := doRegisterTOTP(ctx, s) - s.Assert().NotNil(secret) - doVisit(s, LoginBaseURL) - verifyIsSecondFactorPage(ctx, s) +// Login 1FA and register 2FA. +func (wds *WebDriverSession) doLoginAndRegisterTOTP(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool) string { + wds.doLoginOneFactor(ctx, t, username, password, keepMeLoggedIn, "") + secret := wds.doRegisterTOTP(ctx, t) + wds.doVisit(t, LoginBaseURL) + wds.verifyIsSecondFactorPage(ctx, t) + return secret +} + +// Register a user with TOTP, logout and then authenticate until TOTP-2FA. +func (wds *WebDriverSession) doRegisterAndLogin2FA(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool, targetURL string) string { + // Register TOTP secret and logout. + secret := wds.doRegisterThenLogout(ctx, t, username, password) + wds.doLoginTwoFactor(ctx, t, username, password, false, secret, targetURL) return secret } diff --git a/internal/suites/action_logout.go b/internal/suites/action_logout.go index f82c4ada1..daedab38a 100644 --- a/internal/suites/action_logout.go +++ b/internal/suites/action_logout.go @@ -1,8 +1,12 @@ package suites -import "context" +import ( + "context" + "fmt" + "testing" +) -func doLogout(ctx context.Context, s *SeleniumSuite) { - doVisit(s, "https://login.example.com:8080/#/logout") - verifyIsFirstFactorPage(ctx, s) +func (wds *WebDriverSession) doLogout(ctx context.Context, t *testing.T) { + wds.doVisit(t, fmt.Sprintf("%s%s", LoginBaseURL, "/logout")) + wds.verifyIsFirstFactorPage(ctx, t) } diff --git a/internal/suites/action_mail.go b/internal/suites/action_mail.go index 4f57fc23e..e2f8772b6 100644 --- a/internal/suites/action_mail.go +++ b/internal/suites/action_mail.go @@ -3,8 +3,8 @@ package suites import ( "encoding/json" "fmt" - "log" "regexp" + "testing" "github.com/stretchr/testify/assert" ) @@ -13,22 +13,20 @@ type message struct { ID int `json:"id"` } -func doGetLinkFromLastMail(s *SeleniumSuite) string { - res := doHTTPGetQuery(s, fmt.Sprintf("%s/messages", MailBaseURL)) +func doGetLinkFromLastMail(t *testing.T) string { + res := doHTTPGetQuery(t, fmt.Sprintf("%s/messages", MailBaseURL)) messages := make([]message, 0) err := json.Unmarshal(res, &messages) - assert.NoError(s.T(), err) - assert.Greater(s.T(), len(messages), 0) + assert.NoError(t, err) + assert.Greater(t, len(messages), 0) messageID := messages[len(messages)-1].ID - res = doHTTPGetQuery(s, fmt.Sprintf("%s/messages/%d.html", MailBaseURL, messageID)) + res = doHTTPGetQuery(t, fmt.Sprintf("%s/messages/%d.html", MailBaseURL, messageID)) re := regexp.MustCompile(`.*<\/a>`) matches := re.FindStringSubmatch(string(res)) - if len(matches) != 2 { - log.Fatal("Number of match for link in email is not equal to one") - } + assert.Len(t, matches, 2, "Number of match for link in email is not equal to one") return matches[1] } diff --git a/internal/suites/action_register.go b/internal/suites/action_register.go index 606fb6ba6..4278e9492 100644 --- a/internal/suites/action_register.go +++ b/internal/suites/action_register.go @@ -1,9 +1,12 @@ package suites -import "context" +import ( + "context" + "testing" +) -func doRegisterThenLogout(ctx context.Context, s *SeleniumSuite, username, password string) string { - secret := doLoginAndRegisterTOTP(ctx, s, username, password, false) - doLogout(ctx, s) +func (wds *WebDriverSession) doRegisterThenLogout(ctx context.Context, t *testing.T, username, password string) string { + secret := wds.doLoginAndRegisterTOTP(ctx, t, username, password, false) + wds.doLogout(ctx, t) return secret } diff --git a/internal/suites/action_reset_password.go b/internal/suites/action_reset_password.go new file mode 100644 index 000000000..96db1d23f --- /dev/null +++ b/internal/suites/action_reset_password.go @@ -0,0 +1,35 @@ +package suites + +import ( + "context" + "testing" +) + +func (wds *WebDriverSession) doInitiatePasswordReset(ctx context.Context, t *testing.T, username string) { + wds.WaitElementLocatedByID(ctx, t, "reset-password-button").Click() + // Fill in username + wds.WaitElementLocatedByID(ctx, t, "username-textfield").SendKeys(username) + // And click on the reset button + wds.WaitElementLocatedByID(ctx, t, "reset-button").Click() +} + +func (wds *WebDriverSession) doCompletePasswordReset(ctx context.Context, t *testing.T, newPassword1, newPassword2 string) { + link := doGetLinkFromLastMail(t) + wds.doVisit(t, link) + + wds.WaitElementLocatedByID(ctx, t, "password1-textfield").SendKeys(newPassword1) + wds.WaitElementLocatedByID(ctx, t, "password2-textfield").SendKeys(newPassword2) + wds.WaitElementLocatedByID(ctx, t, "reset-button").Click() +} + +func (wds *WebDriverSession) doSuccessfullyCompletePasswordReset(ctx context.Context, t *testing.T, newPassword1, newPassword2 string) { + wds.doCompletePasswordReset(ctx, t, newPassword1, newPassword2) + wds.verifyIsFirstFactorPage(ctx, t) +} + +func (wds *WebDriverSession) doResetPassword(ctx context.Context, t *testing.T, username, newPassword1, newPassword2 string) { + wds.doInitiatePasswordReset(ctx, t, username) + // then wait for the "email sent notification" + wds.verifyMailNotificationDisplayed(ctx, t) + wds.doSuccessfullyCompletePasswordReset(ctx, t, newPassword1, newPassword2) +} diff --git a/internal/suites/action_totp.go b/internal/suites/action_totp.go index c3076ad68..015931a71 100644 --- a/internal/suites/action_totp.go +++ b/internal/suites/action_totp.go @@ -2,24 +2,35 @@ package suites import ( "context" + "testing" "time" "github.com/pquerna/otp/totp" + "github.com/stretchr/testify/assert" ) -func doRegisterTOTP(ctx context.Context, s *SeleniumSuite) string { - WaitElementLocatedByClassName(ctx, s, "register-totp").Click() - verifyBodyContains(ctx, s, "Please check your e-mails") - link := doGetLinkFromLastMail(s) - doVisit(s, link) - secret, err := WaitElementLocatedByClassName(ctx, s, "base32-secret").Text() - s.Assert().NoError(err) +func (wds *WebDriverSession) doRegisterTOTP(ctx context.Context, t *testing.T) string { + wds.WaitElementLocatedByID(ctx, t, "register-link").Click() + wds.verifyMailNotificationDisplayed(ctx, t) + link := doGetLinkFromLastMail(t) + wds.doVisit(t, link) + secret, err := wds.WaitElementLocatedByID(ctx, t, "base32-secret").GetAttribute("value") + assert.NoError(t, err) + assert.NotEqual(t, "", secret) + assert.NotNil(t, secret) return secret } -func doValidateTOTP(ctx context.Context, s *SeleniumSuite, secret string) { - code, err := totp.GenerateCode(secret, time.Now()) - s.Assert().NoError(err) - WaitElementLocatedByID(ctx, s, "totp-token").SendKeys(code) - WaitElementLocatedByID(ctx, s, "totp-button").Click() +func (wds *WebDriverSession) doEnterOTP(ctx context.Context, t *testing.T, code string) { + inputs := wds.WaitElementsLocatedByCSSSelector(ctx, t, "#otp-input input") + + for i := 0; i < 6; i++ { + inputs[i].SendKeys(string(code[i])) + } +} + +func (wds *WebDriverSession) doValidateTOTP(ctx context.Context, t *testing.T, secret string) { + code, err := totp.GenerateCode(secret, time.Now()) + assert.NoError(t, err) + wds.doEnterOTP(ctx, t, code) } diff --git a/internal/suites/action_visit.go b/internal/suites/action_visit.go index cc7f4d9f7..76a4b1d50 100644 --- a/internal/suites/action_visit.go +++ b/internal/suites/action_visit.go @@ -3,25 +3,25 @@ package suites import ( "context" "fmt" - "net/url" + "testing" "github.com/stretchr/testify/assert" ) -func doVisit(s *SeleniumSuite, url string) { - err := s.WebDriver().Get(url) - assert.NoError(s.T(), err) +func (wds *WebDriverSession) doVisit(t *testing.T, url string) { + err := wds.WebDriver.Get(url) + assert.NoError(t, err) } -func doVisitAndVerifyURLIs(ctx context.Context, s *SeleniumSuite, url string) { - doVisit(s, url) - verifyURLIs(ctx, s, url) +func (wds *WebDriverSession) doVisitAndVerifyURLIs(ctx context.Context, t *testing.T, url string) { + wds.doVisit(t, url) + wds.verifyURLIs(ctx, t, url) } -func doVisitLoginPage(ctx context.Context, s *SeleniumSuite, targetURL string) { +func (wds *WebDriverSession) doVisitLoginPage(ctx context.Context, t *testing.T, targetURL string) { suffix := "" if targetURL != "" { - suffix = fmt.Sprintf("?rd=%s", url.QueryEscape(targetURL)) + suffix = fmt.Sprintf("?rd=%s", targetURL) } - doVisitAndVerifyURLIs(ctx, s, fmt.Sprintf("%s%s", LoginBaseURL, suffix)) + wds.doVisitAndVerifyURLIs(ctx, t, fmt.Sprintf("%s/%s", LoginBaseURL, suffix)) } diff --git a/internal/suites/constants.go b/internal/suites/constants.go index 443c73ca2..968a581d1 100644 --- a/internal/suites/constants.go +++ b/internal/suites/constants.go @@ -6,7 +6,7 @@ import "fmt" var BaseDomain = "example.com:8080" // LoginBaseURL the base URL of the login portal -var LoginBaseURL = fmt.Sprintf("https://login.%s/", BaseDomain) +var LoginBaseURL = fmt.Sprintf("https://login.%s", BaseDomain) // SingleFactorBaseURL the base URL of the singlefactor domain var SingleFactorBaseURL = fmt.Sprintf("https://singlefactor.%s", BaseDomain) @@ -18,4 +18,25 @@ var AdminBaseURL = fmt.Sprintf("https://admin.%s", BaseDomain) var MailBaseURL = fmt.Sprintf("https://mail.%s", BaseDomain) // HomeBaseURL the base URL of the home domain -var HomeBaseURL = fmt.Sprintf("https://home.%s/", BaseDomain) +var HomeBaseURL = fmt.Sprintf("https://home.%s", BaseDomain) + +// PublicBaseURL the base URL of the public domain +var PublicBaseURL = fmt.Sprintf("https://public.%s", BaseDomain) + +// SecureBaseURL the base URL of the secure domain +var SecureBaseURL = fmt.Sprintf("https://secure.%s", BaseDomain) + +// DevBaseURL the base URL of the dev domain +var DevBaseURL = fmt.Sprintf("https://dev.%s", BaseDomain) + +// MX1MailBaseURL the base URL of the mx1.mail domain +var MX1MailBaseURL = fmt.Sprintf("https://mx1.mail.%s", BaseDomain) + +// MX2MailBaseURL the base URL of the mx2.mail domain +var MX2MailBaseURL = fmt.Sprintf("https://mx2.mail.%s", BaseDomain) + +// DuoBaseURL the base URL of the Duo configuration API +var DuoBaseURL = "https://duo.example.com" + +// AutheliaBaseURL the base URL of Authelia service +var AutheliaBaseURL = "http://authelia.example.com:9091" diff --git a/internal/suites/docker.go b/internal/suites/docker.go index 9be0f189c..2a7370547 100644 --- a/internal/suites/docker.go +++ b/internal/suites/docker.go @@ -2,7 +2,6 @@ package suites import ( "fmt" - "os" "os/exec" "strings" @@ -21,41 +20,35 @@ func NewDockerEnvironment(files []string) *DockerEnvironment { } func (de *DockerEnvironment) createCommandWithStdout(cmd string) *exec.Cmd { - dockerCmdLine := "docker-compose -f " + strings.Join(de.dockerComposeFiles, " -f ") + " " + cmd + dockerCmdLine := fmt.Sprintf("docker-compose -f %s %s", strings.Join(de.dockerComposeFiles, " -f "), cmd) log.Trace(dockerCmdLine) return utils.CommandWithStdout("bash", "-c", dockerCmdLine) } func (de *DockerEnvironment) createCommand(cmd string) *exec.Cmd { - dockerCmdLine := "docker-compose -f " + strings.Join(de.dockerComposeFiles, " -f ") + " " + cmd + dockerCmdLine := fmt.Sprintf("docker-compose -f %s %s", strings.Join(de.dockerComposeFiles, " -f "), cmd) log.Trace(dockerCmdLine) - return exec.Command("bash", "-c", dockerCmdLine) + return utils.Command("bash", "-c", dockerCmdLine) } // Up spawn a docker environment -func (de *DockerEnvironment) Up(suitePath string) error { - cmd := de.createCommandWithStdout("up -d") - cmd.Env = append(os.Environ(), "SUITE_PATH="+suitePath) - return cmd.Run() +func (de *DockerEnvironment) Up() error { + return de.createCommandWithStdout("up -d").Run() } // Restart restarts a service -func (de *DockerEnvironment) Restart(suitePath, service string) error { - cmd := de.createCommandWithStdout(fmt.Sprintf("restart %s", service)) - cmd.Env = append(os.Environ(), "SUITE_PATH="+suitePath) - return cmd.Run() +func (de *DockerEnvironment) Restart(service string) error { + return de.createCommandWithStdout(fmt.Sprintf("restart %s", service)).Run() } // Down spawn a docker environment -func (de *DockerEnvironment) Down(suitePath string) error { - cmd := de.createCommandWithStdout("down -v") - cmd.Env = append(os.Environ(), "SUITE_PATH="+suitePath) - return cmd.Run() +func (de *DockerEnvironment) Down() error { + return de.createCommandWithStdout("down -v").Run() } // Logs get logs of a given service of the environment func (de *DockerEnvironment) Logs(service string, flags []string) (string, error) { - cmd := de.createCommand("logs " + strings.Join(flags, " ") + " " + service) + cmd := de.createCommand(fmt.Sprintf("logs %s %s", strings.Join(flags, " "), service)) content, err := cmd.Output() return string(content), err } diff --git a/internal/suites/duo.go b/internal/suites/duo.go new file mode 100644 index 000000000..400c1ef25 --- /dev/null +++ b/internal/suites/duo.go @@ -0,0 +1,35 @@ +package suites + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +// DuoPolicy a type of policy +type DuoPolicy int32 + +const ( + // Deny deny policy + Deny DuoPolicy = iota + // Allow allow policy + Allow DuoPolicy = iota +) + +// ConfigureDuo configure duo api to allow or block auth requests +func ConfigureDuo(t *testing.T, allowDeny DuoPolicy) { + url := fmt.Sprintf("%s/allow", DuoBaseURL) + if allowDeny == Deny { + url = fmt.Sprintf("%s/deny", DuoBaseURL) + } + + req, err := http.NewRequest("POST", url, nil) + assert.NoError(t, err) + + client := NewHTTPClient() + res, err := client.Do(req) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode) +} diff --git a/internal/suites/http.go b/internal/suites/http.go new file mode 100644 index 000000000..6d844026e --- /dev/null +++ b/internal/suites/http.go @@ -0,0 +1,21 @@ +package suites + +import ( + "crypto/tls" + "net/http" +) + +// NewHTTPClient create a new client skipping TLS verification and not redirecting +func NewHTTPClient() *http.Client { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + return &http.Client{ + Transport: tr, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} diff --git a/internal/suites/scenario_available_methods_test.go b/internal/suites/scenario_available_methods_test.go new file mode 100644 index 000000000..a0cacdc81 --- /dev/null +++ b/internal/suites/scenario_available_methods_test.go @@ -0,0 +1,87 @@ +package suites + +import ( + "context" + "log" + "time" + + "github.com/tebeka/selenium" +) + +type AvailableMethodsScenario struct { + *SeleniumSuite + + methods []string +} + +func NewAvailableMethodsScenario(methods []string) *AvailableMethodsScenario { + return &AvailableMethodsScenario{ + SeleniumSuite: new(SeleniumSuite), + methods: methods, + } +} + +func (s *AvailableMethodsScenario) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.SeleniumSuite.WebDriverSession = wds +} + +func (s *AvailableMethodsScenario) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *AvailableMethodsScenario) SetupTest() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) +} + +func IsStringInList(str string, list []string) bool { + for _, v := range list { + if v == str { + return true + } + } + return false +} + +func (s *AvailableMethodsScenario) TestShouldCheckAvailableMethods() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "") + + methodsButton := s.WaitElementLocatedByID(ctx, s.T(), "methods-button") + err := methodsButton.Click() + s.Assert().NoError(err) + + methodsDialog := s.WaitElementLocatedByID(ctx, s.T(), "methods-dialog") + options, err := methodsDialog.FindElements(selenium.ByClassName, "method-option") + s.Assert().NoError(err) + s.Assert().Len(options, len(s.methods)) + + optionsList := make([]string, 0) + for _, o := range options { + txt, err := o.Text() + s.Assert().NoError(err) + optionsList = append(optionsList, txt) + } + + s.Assert().Len(optionsList, len(s.methods)) + + for _, m := range s.methods { + s.Assert().True(IsStringInList(m, optionsList)) + } +} diff --git a/internal/suites/scenario_backend_protection_test.go b/internal/suites/scenario_backend_protection_test.go new file mode 100644 index 000000000..253caf227 --- /dev/null +++ b/internal/suites/scenario_backend_protection_test.go @@ -0,0 +1,57 @@ +package suites + +import ( + "crypto/tls" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/suite" +) + +type BackendProtectionScenario struct { + suite.Suite +} + +func NewBackendProtectionScenario() *BackendProtectionScenario { + return &BackendProtectionScenario{} +} + +func (s *BackendProtectionScenario) AssertRequestStatusCode(method, url string, expectedStatusCode int) { + s.Run(url, func() { + req, err := http.NewRequest(method, url, nil) + s.Assert().NoError(err) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{ + Transport: tr, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + res, err := client.Do(req) + s.Assert().NoError(err) + s.Assert().Equal(res.StatusCode, expectedStatusCode) + }) +} + +func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() { + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/register", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign_request", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/preferences", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/preferences", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/available", AutheliaBaseURL), 403) + + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/finish", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp/identity/start", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp/identity/finish", AutheliaBaseURL), 403) +} + +func TestRunBackendProtection(t *testing.T) { + suite.Run(t, NewBackendProtectionScenario()) +} diff --git a/internal/suites/scenario_bypass_policy_test.go b/internal/suites/scenario_bypass_policy_test.go new file mode 100644 index 000000000..bb774473e --- /dev/null +++ b/internal/suites/scenario_bypass_policy_test.go @@ -0,0 +1,63 @@ +package suites + +import ( + "context" + "fmt" + "log" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type BypassPolicyScenario struct { + *SeleniumSuite +} + +func NewBypassPolicyScenario() *BypassPolicyScenario { + return &BypassPolicyScenario{ + SeleniumSuite: new(SeleniumSuite), + } +} + +func (s *BypassPolicyScenario) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *BypassPolicyScenario) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *BypassPolicyScenario) SetupTest() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) +} + +func (s *BypassPolicyScenario) TestShouldAccessPublicResource() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doVisit(s.T(), AdminBaseURL) + s.verifyIsFirstFactorPage(ctx, s.T()) + + s.doVisit(s.T(), fmt.Sprintf("%s/secret.html", PublicBaseURL)) + s.verifySecretAuthorized(ctx, s.T()) +} + +func TestBypassPolicyScenario(t *testing.T) { + suite.Run(t, NewBypassPolicyScenario()) +} diff --git a/internal/suites/scenario_custom_headers_test.go b/internal/suites/scenario_custom_headers_test.go new file mode 100644 index 000000000..c908ab342 --- /dev/null +++ b/internal/suites/scenario_custom_headers_test.go @@ -0,0 +1,78 @@ +package suites + +import ( + "context" + "fmt" + "log" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/tebeka/selenium" +) + +type CustomHeadersScenario struct { + *SeleniumSuite +} + +func NewCustomHeadersScenario() *CustomHeadersScenario { + return &CustomHeadersScenario{ + SeleniumSuite: new(SeleniumSuite), + } +} + +func (s *CustomHeadersScenario) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *CustomHeadersScenario) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *CustomHeadersScenario) SetupTest() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) +} + +func (s *CustomHeadersScenario) TestShouldNotForwardCustomHeaderForUnauthenticatedUser() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doVisit(s.T(), fmt.Sprintf("%s/headers", PublicBaseURL)) + + body, err := s.WebDriver().FindElement(selenium.ByTagName, "body") + s.Assert().NoError(err) + s.WaitElementTextContains(ctx, s.T(), body, "httpbin:8000") +} + +func (s *CustomHeadersScenario) TestShouldForwardCustomHeaderForAuthenticatedUser() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + targetURL := fmt.Sprintf("%s/headers", PublicBaseURL) + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL) + s.verifyURLIs(ctx, s.T(), targetURL) + + body, err := s.WebDriver().FindElement(selenium.ByTagName, "body") + s.Assert().NoError(err) + s.WaitElementTextContains(ctx, s.T(), body, "\"Custom-Forwarded-User\": \"john\"") + s.WaitElementTextContains(ctx, s.T(), body, "\"Custom-Forwarded-Groups\": \"admins,dev\"") +} + +func TestCustomHeadersScenario(t *testing.T) { + suite.Run(t, NewCustomHeadersScenario()) +} diff --git a/internal/suites/scenario_inactivity_test.go b/internal/suites/scenario_inactivity_test.go new file mode 100644 index 000000000..87c779c50 --- /dev/null +++ b/internal/suites/scenario_inactivity_test.go @@ -0,0 +1,117 @@ +package suites + +import ( + "context" + "fmt" + "log" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type InactivityScenario struct { + *SeleniumSuite + secret string +} + +func NewInactivityScenario() *InactivityScenario { + return &InactivityScenario{ + SeleniumSuite: new(SeleniumSuite), + } +} + +func (s *InactivityScenario) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL) + s.secret = s.doRegisterAndLogin2FA(ctx, s.T(), "john", "password", false, targetURL) + s.verifySecretAuthorized(ctx, s.T()) +} + +func (s *InactivityScenario) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *InactivityScenario) SetupTest() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) +} + +func (s *InactivityScenario) TestShouldRequireReauthenticationAfterInactivityPeriod() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL) + s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, s.secret, "") + + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) + + time.Sleep(6 * time.Second) + + s.doVisit(s.T(), targetURL) + s.verifyIsFirstFactorPage(ctx, s.T()) +} + +func (s *InactivityScenario) TestShouldRequireReauthenticationAfterCookieExpiration() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL) + s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, s.secret, "") + + for i := 0; i < 3; i++ { + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) + + time.Sleep(2 * time.Second) + s.doVisit(s.T(), targetURL) + s.verifySecretAuthorized(ctx, s.T()) + } + + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) + + time.Sleep(2 * time.Second) + + s.doVisit(s.T(), targetURL) + s.verifyIsFirstFactorPage(ctx, s.T()) +} + +func (s *InactivityScenario) TestShouldDisableCookieExpirationAndInactivity() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL) + s.doLoginTwoFactor(ctx, s.T(), "john", "password", true, s.secret, "") + + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) + + time.Sleep(9 * time.Second) + + s.doVisit(s.T(), targetURL) + s.verifySecretAuthorized(ctx, s.T()) +} + +func TestInactivityScenario(t *testing.T) { + suite.Run(t, NewInactivityScenario()) +} diff --git a/internal/suites/scenario_one_factor_test.go b/internal/suites/scenario_one_factor_test.go index 3dbd480f0..45f74b7ce 100644 --- a/internal/suites/scenario_one_factor_test.go +++ b/internal/suites/scenario_one_factor_test.go @@ -14,7 +14,7 @@ type OneFactorSuite struct { *SeleniumSuite } -func NewOneFactorSuite() *OneFactorSuite { +func NewOneFactorScenario() *OneFactorSuite { return &OneFactorSuite{ SeleniumSuite: new(SeleniumSuite), } @@ -27,7 +27,7 @@ func (s *OneFactorSuite) SetupSuite() { log.Fatal(err) } - s.SeleniumSuite.WebDriverSession = wds + s.WebDriverSession = wds } func (s *OneFactorSuite) TearDownSuite() { @@ -42,9 +42,9 @@ func (s *OneFactorSuite) SetupTest() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - doLogout(ctx, s.SeleniumSuite) - doVisit(s.SeleniumSuite, HomeBaseURL) - verifyURLIs(ctx, s.SeleniumSuite, HomeBaseURL) + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) } func (s *OneFactorSuite) TestShouldAuthorizeSecretAfterOneFactor() { @@ -52,8 +52,8 @@ func (s *OneFactorSuite) TestShouldAuthorizeSecretAfterOneFactor() { defer cancel() targetURL := fmt.Sprintf("%s/secret.html", SingleFactorBaseURL) - doLoginOneFactor(ctx, s.SeleniumSuite, "john", "password", false, targetURL) - verifySecretAuthorized(ctx, s.SeleniumSuite) + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL) + s.verifySecretAuthorized(ctx, s.T()) } func (s *OneFactorSuite) TestShouldRedirectToSecondFactor() { @@ -61,8 +61,8 @@ func (s *OneFactorSuite) TestShouldRedirectToSecondFactor() { defer cancel() targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL) - doLoginOneFactor(ctx, s.SeleniumSuite, "john", "password", false, targetURL) - verifyIsSecondFactorPage(ctx, s.SeleniumSuite) + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL) + s.verifyIsSecondFactorPage(ctx, s.T()) } func (s *OneFactorSuite) TestShouldDenyAccessOnBadPassword() { @@ -70,11 +70,11 @@ func (s *OneFactorSuite) TestShouldDenyAccessOnBadPassword() { defer cancel() targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL) - doLoginOneFactor(ctx, s.SeleniumSuite, "john", "bad-password", false, targetURL) - verifyIsFirstFactorPage(ctx, s.SeleniumSuite) - verifyNotificationDisplayed(ctx, s.SeleniumSuite, "Authentication failed. Check your credentials.") + s.doLoginOneFactor(ctx, s.T(), "john", "bad-password", false, targetURL) + s.verifyIsFirstFactorPage(ctx, s.T()) + s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.") } func TestRunOneFactor(t *testing.T) { - suite.Run(t, NewOneFactorSuite()) + suite.Run(t, NewOneFactorScenario()) } diff --git a/internal/suites/scenario_redirection_check_test.go b/internal/suites/scenario_redirection_check_test.go new file mode 100644 index 000000000..19c759165 --- /dev/null +++ b/internal/suites/scenario_redirection_check_test.go @@ -0,0 +1,82 @@ +package suites + +import ( + "context" + "log" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type RedirectionCheckScenario struct { + *SeleniumSuite +} + +func NewRedirectionCheckScenario() *RedirectionCheckScenario { + return &RedirectionCheckScenario{ + SeleniumSuite: new(SeleniumSuite), + } +} + +func (s *RedirectionCheckScenario) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *RedirectionCheckScenario) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *RedirectionCheckScenario) SetupTest() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) +} + +var redirectionAuthorizations = map[string]bool{ + // external website + "https://www.google.fr": false, + // Not the right domain + "https://public.example.com.a:8080/secret.html": false, + // Not https + "http://secure.example.com:8080/secret.html": false, + // Domain handled by Authelia + "https://secure.example.com:8080/secret.html": true, +} + +func (s *RedirectionCheckScenario) TestShouldRedirectOnlyWhenDomainIsHandledByAuthelia() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password") + + for url, redirected := range redirectionAuthorizations { + s.T().Run(url, func(t *testing.T) { + s.doLoginTwoFactor(ctx, t, "john", "password", false, secret, url) + time.Sleep(1 * time.Second) + if redirected { + s.verifySecretAuthorized(ctx, t) + } else { + s.WaitElementLocatedByClassName(ctx, t, "success-icon") + } + s.doLogout(ctx, t) + }) + } +} + +func TestRedirectionCheckScenario(t *testing.T) { + suite.Run(t, NewRedirectionCheckScenario()) +} diff --git a/internal/suites/scenario_regulation_test.go b/internal/suites/scenario_regulation_test.go new file mode 100644 index 000000000..fef4ca379 --- /dev/null +++ b/internal/suites/scenario_regulation_test.go @@ -0,0 +1,83 @@ +package suites + +import ( + "context" + "log" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/tebeka/selenium" +) + +type RegulationScenario struct { + *SeleniumSuite +} + +func NewRegulationScenario() *RegulationScenario { + return &RegulationScenario{ + SeleniumSuite: new(SeleniumSuite), + } +} + +func (s *RegulationScenario) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *RegulationScenario) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *RegulationScenario) SetupTest() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) +} + +func (s *RegulationScenario) TestShouldBanUserAfterTooManyAttempt() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + s.doVisitLoginPage(ctx, s.T(), "") + s.doFillLoginPageAndClick(ctx, s.T(), "john", "bad-password", false) + s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.") + + for i := 0; i < 3; i++ { + s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click() + time.Sleep(2 * time.Second) + } + + // Reset password field + s.WaitElementLocatedByID(ctx, s.T(), "password-textfield"). + SendKeys(selenium.ControlKey + "a" + selenium.BackspaceKey) + + // And enter the correct password + s.WaitElementLocatedByID(ctx, s.T(), "password-textfield").SendKeys("password") + s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click() + s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.") + + time.Sleep(1 * time.Second) + s.verifyIsFirstFactorPage(ctx, s.T()) + + time.Sleep(9 * time.Second) + + s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click() + s.verifyIsSecondFactorPage(ctx, s.T()) +} + +func TestBlacklistingScenario(t *testing.T) { + suite.Run(t, NewRegulationScenario()) +} diff --git a/internal/suites/scenario_reset_password_test.go b/internal/suites/scenario_reset_password_test.go new file mode 100644 index 000000000..15b3df5dd --- /dev/null +++ b/internal/suites/scenario_reset_password_test.go @@ -0,0 +1,101 @@ +package suites + +import ( + "context" + "log" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type ResetPasswordScenario struct { + *SeleniumSuite +} + +func NewResetPasswordScenario() *ResetPasswordScenario { + return &ResetPasswordScenario{SeleniumSuite: new(SeleniumSuite)} +} + +func (s *ResetPasswordScenario) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *ResetPasswordScenario) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *ResetPasswordScenario) SetupTest() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) +} + +func (s *ResetPasswordScenario) TestShouldResetPassword() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + s.doVisit(s.T(), LoginBaseURL) + s.verifyIsFirstFactorPage(ctx, s.T()) + + // Reset the password to abc + s.doResetPassword(ctx, s.T(), "john", "abc", "abc") + + // Try to login with the old password + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "") + s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.") + + // Try to login with the new password + s.doLoginOneFactor(ctx, s.T(), "john", "abc", false, "") + + // Logout + s.doLogout(ctx, s.T()) + + // Reset the original password + s.doResetPassword(ctx, s.T(), "john", "password", "password") +} + +func (s *ResetPasswordScenario) TestShouldMakeAttackerThinkPasswordResetIsInitiated() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + s.doVisit(s.T(), LoginBaseURL) + s.verifyIsFirstFactorPage(ctx, s.T()) + + // Try to initiate a password reset of an inexistant user + s.doInitiatePasswordReset(ctx, s.T(), "i_dont_exist") + + // Check that the notification make the attacker thinks the process is initiated + s.verifyMailNotificationDisplayed(ctx, s.T()) +} + +func (s *ResetPasswordScenario) TestShouldLetUserNoticeThereIsAPasswordMismatch() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + s.doVisit(s.T(), LoginBaseURL) + s.verifyIsFirstFactorPage(ctx, s.T()) + + s.doInitiatePasswordReset(ctx, s.T(), "john") + s.verifyMailNotificationDisplayed(ctx, s.T()) + + s.doCompletePasswordReset(ctx, s.T(), "password", "another_password") + s.verifyNotificationDisplayed(ctx, s.T(), "Passwords do not match.") +} + +func TestRunResetPasswordScenario(t *testing.T) { + suite.Run(t, NewResetPasswordScenario()) +} diff --git a/internal/suites/scenario_two_factor_test.go b/internal/suites/scenario_two_factor_test.go index abfa75967..4302daddf 100644 --- a/internal/suites/scenario_two_factor_test.go +++ b/internal/suites/scenario_two_factor_test.go @@ -14,7 +14,7 @@ type TwoFactorSuite struct { *SeleniumSuite } -func NewTwoFactorSuite() *TwoFactorSuite { +func NewTwoFactorScenario() *TwoFactorSuite { return &TwoFactorSuite{ SeleniumSuite: new(SeleniumSuite), } @@ -27,7 +27,7 @@ func (s *TwoFactorSuite) SetupSuite() { log.Fatal(err) } - s.SeleniumSuite.WebDriverSession = wds + s.WebDriverSession = wds } func (s *TwoFactorSuite) TearDownSuite() { @@ -42,24 +42,44 @@ func (s *TwoFactorSuite) SetupTest() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - doLogout(ctx, s.SeleniumSuite) - doVisit(s.SeleniumSuite, HomeBaseURL) - verifyURLIs(ctx, s.SeleniumSuite, HomeBaseURL) + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) } func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() // Register TOTP secret and logout. - secret := doRegisterThenLogout(ctx, s.SeleniumSuite, "john", "password") + secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password") targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL) - doLoginTwoFactor(ctx, s.SeleniumSuite, "john", "password", false, secret, targetURL) + s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, targetURL) + s.verifySecretAuthorized(ctx, s.T()) - verifySecretAuthorized(ctx, s.SeleniumSuite) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) + + s.doVisit(s.T(), targetURL) + s.verifySecretAuthorized(ctx, s.T()) +} + +func (s *TwoFactorSuite) TestShouldFailTwoFactor() { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + // Register TOTP secret and logout. + s.doRegisterThenLogout(ctx, s.T(), "john", "password") + + wrongPasscode := "123456" + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "") + s.verifyIsSecondFactorPage(ctx, s.T()) + s.doEnterOTP(ctx, s.T(), wrongPasscode) + + s.verifyNotificationDisplayed(ctx, s.T(), "The one-time password might be wrong") } func TestRunTwoFactor(t *testing.T) { - suite.Run(t, NewTwoFactorSuite()) + suite.Run(t, NewTwoFactorScenario()) } diff --git a/internal/suites/scenario_user_preferences_test.go b/internal/suites/scenario_user_preferences_test.go new file mode 100644 index 000000000..843dabff8 --- /dev/null +++ b/internal/suites/scenario_user_preferences_test.go @@ -0,0 +1,99 @@ +package suites + +import ( + "context" + "log" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type UserPreferencesScenario struct { + *SeleniumSuite +} + +func NewUserPreferencesScenario() *UserPreferencesScenario { + return &UserPreferencesScenario{ + SeleniumSuite: new(SeleniumSuite), + } +} + +func (s *UserPreferencesScenario) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *UserPreferencesScenario) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *UserPreferencesScenario) SetupTest() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doLogout(ctx, s.T()) + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) +} + +func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Authenticate + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "") + s.verifyIsSecondFactorPage(ctx, s.T()) + + // And select OTP method + s.doChangeMethod(ctx, s.T(), "one-time-password") + s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method") + + // Then switch to push notification method + s.doChangeMethod(ctx, s.T(), "push-notification") + s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method") + + // Switch context to clean up state in portal. + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) + + // Then go back to portal. + s.doVisit(s.T(), LoginBaseURL) + s.verifyIsSecondFactorPage(ctx, s.T()) + + // And check the latest method is still used. + s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method") + // Meaning the authentication is successful + s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon") + + // Logout the user and see what user 'harry' sees. + s.doLogout(ctx, s.T()) + s.doLoginOneFactor(ctx, s.T(), "harry", "password", false, "") + s.verifyIsSecondFactorPage(ctx, s.T()) + s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method") + s.doLogout(ctx, s.T()) + s.verifyIsFirstFactorPage(ctx, s.T()) + + // Then log back as previous user and verify the push notification is still the default method + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "") + s.verifyIsSecondFactorPage(ctx, s.T()) + s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method") + s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon") + + // Eventually restore the default method + s.doChangeMethod(ctx, s.T(), "one-time-password") + s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method") +} + +func TestUserPreferencesScenario(t *testing.T) { + suite.Run(t, NewUserPreferencesScenario()) +} diff --git a/internal/suites/suite_bypass_all.go b/internal/suites/suite_bypass_all.go index a9d332a27..555a1f28e 100644 --- a/internal/suites/suite_bypass_all.go +++ b/internal/suites/suite_bypass_all.go @@ -9,6 +9,7 @@ var bypassAllSuiteName = "BypassAll" func init() { dockerEnvironment := NewDockerEnvironment([]string{ "docker-compose.yml", + "internal/suites/BypassAll/docker-compose.yml", "example/compose/authelia/docker-compose.backend.yml", "example/compose/authelia/docker-compose.frontend.yml", "example/compose/nginx/backend/docker-compose.yml", @@ -19,7 +20,7 @@ func init() { }) setup := func(suitePath string) error { - if err := dockerEnvironment.Up(suitePath); err != nil { + if err := dockerEnvironment.Up(); err != nil { return err } @@ -27,7 +28,7 @@ func init() { } teardown := func(suitePath string) error { - return dockerEnvironment.Down(suitePath) + return dockerEnvironment.Down() } GlobalRegistry.Register(bypassAllSuiteName, Suite{ diff --git a/internal/suites/suite_bypass_all_test.go b/internal/suites/suite_bypass_all_test.go index 6f8511a10..3e21c719c 100644 --- a/internal/suites/suite_bypass_all_test.go +++ b/internal/suites/suite_bypass_all_test.go @@ -1,7 +1,13 @@ package suites import ( + "context" + "fmt" + "log" "testing" + "time" + + "github.com/stretchr/testify/suite" ) type BypassAllSuite struct { @@ -12,6 +18,36 @@ func NewBypassAllSuite() *BypassAllSuite { return &BypassAllSuite{SeleniumSuite: new(SeleniumSuite)} } -func TestBypassAllSuite(t *testing.T) { - RunTypescriptSuite(t, bypassAllSuiteName) +func (s *BypassAllSuite) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *BypassAllSuite) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *BypassAllSuite) TestShouldAccessPublicResource() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doVisit(s.T(), fmt.Sprintf("%s/secret.html", AdminBaseURL)) + s.verifySecretAuthorized(ctx, s.T()) + + s.doVisit(s.T(), fmt.Sprintf("%s/secret.html", PublicBaseURL)) + s.verifySecretAuthorized(ctx, s.T()) +} + +func TestBypassAllSuite(t *testing.T) { + suite.Run(t, NewBypassAllSuite()) + suite.Run(t, NewCustomHeadersScenario()) } diff --git a/internal/suites/suite_duo_push.go b/internal/suites/suite_duo_push.go index 516e6230f..e90633f87 100644 --- a/internal/suites/suite_duo_push.go +++ b/internal/suites/suite_duo_push.go @@ -9,6 +9,7 @@ var duoPushSuiteName = "DuoPush" func init() { dockerEnvironment := NewDockerEnvironment([]string{ "docker-compose.yml", + "internal/suites/DuoPush/docker-compose.yml", "example/compose/authelia/docker-compose.backend.yml", "example/compose/authelia/docker-compose.frontend.yml", "example/compose/nginx/backend/docker-compose.yml", @@ -17,7 +18,7 @@ func init() { }) setup := func(suitePath string) error { - if err := dockerEnvironment.Up(suitePath); err != nil { + if err := dockerEnvironment.Up(); err != nil { return err } @@ -25,7 +26,7 @@ func init() { } teardown := func(suitePath string) error { - return dockerEnvironment.Down(suitePath) + return dockerEnvironment.Down() } GlobalRegistry.Register(duoPushSuiteName, Suite{ diff --git a/internal/suites/suite_duo_push_test.go b/internal/suites/suite_duo_push_test.go index 91f842590..143597f89 100644 --- a/internal/suites/suite_duo_push_test.go +++ b/internal/suites/suite_duo_push_test.go @@ -1,7 +1,12 @@ package suites import ( + "context" + "log" "testing" + "time" + + "github.com/stretchr/testify/suite" ) type DuoPushSuite struct { @@ -12,6 +17,58 @@ func NewDuoPushSuite() *DuoPushSuite { return &DuoPushSuite{SeleniumSuite: new(SeleniumSuite)} } -func TestDuoPushSuite(t *testing.T) { - RunTypescriptSuite(t, duoPushSuiteName) +func (s *DuoPushSuite) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *DuoPushSuite) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *DuoPushSuite) TearDownTest() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + s.doChangeMethod(ctx, s.T(), "one-time-password") +} + +func (s *DuoPushSuite) TestShouldSucceedAuthentication() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + ConfigureDuo(s.T(), Allow) + + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "") + s.doChangeMethod(ctx, s.T(), "push-notification") + s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon") +} + +func (s *DuoPushSuite) TestShouldFailAuthentication() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + ConfigureDuo(s.T(), Deny) + + s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "") + s.doChangeMethod(ctx, s.T(), "push-notification") + s.WaitElementLocatedByClassName(ctx, s.T(), "failure-icon") +} + +func TestDuoPushSuite(t *testing.T) { + suite.Run(t, NewDuoPushSuite()) + suite.Run(t, NewAvailableMethodsScenario([]string{ + "ONE-TIME PASSWORD", + "PUSH NOTIFICATION", + })) + suite.Run(t, NewUserPreferencesScenario()) } diff --git a/internal/suites/suite_high_availability.go b/internal/suites/suite_high_availability.go index e2412a97e..b40b656b5 100644 --- a/internal/suites/suite_high_availability.go +++ b/internal/suites/suite_high_availability.go @@ -6,31 +6,32 @@ import ( var highAvailabilitySuiteName = "HighAvailability" -func init() { - dockerEnvironment := NewDockerEnvironment([]string{ - "docker-compose.yml", - "example/compose/authelia/docker-compose.backend.yml", - "example/compose/authelia/docker-compose.frontend.yml", - "example/compose/mariadb/docker-compose.yml", - "example/compose/redis/docker-compose.yml", - "example/compose/nginx/backend/docker-compose.yml", - "example/compose/nginx/portal/docker-compose.yml", - "example/compose/smtp/docker-compose.yml", - "example/compose/httpbin/docker-compose.yml", - "example/compose/ldap/docker-compose.admin.yml", // This is just used for administration, not for testing. - "example/compose/ldap/docker-compose.yml", - }) +var haDockerEnvironment = NewDockerEnvironment([]string{ + "docker-compose.yml", + "internal/suites/HighAvailability/docker-compose.yml", + "example/compose/authelia/docker-compose.backend.yml", + "example/compose/authelia/docker-compose.frontend.yml", + "example/compose/mariadb/docker-compose.yml", + "example/compose/redis/docker-compose.yml", + "example/compose/nginx/backend/docker-compose.yml", + "example/compose/nginx/portal/docker-compose.yml", + "example/compose/smtp/docker-compose.yml", + "example/compose/httpbin/docker-compose.yml", + "example/compose/ldap/docker-compose.admin.yml", // This is just used for administration, not for testing. + "example/compose/ldap/docker-compose.yml", +}) +func init() { setup := func(suitePath string) error { - if err := dockerEnvironment.Up(suitePath); err != nil { + if err := haDockerEnvironment.Up(); err != nil { return err } - return waitUntilAutheliaIsReady(dockerEnvironment) + return waitUntilAutheliaIsReady(haDockerEnvironment) } teardown := func(suitePath string) error { - return dockerEnvironment.Down(suitePath) + return haDockerEnvironment.Down() } GlobalRegistry.Register(highAvailabilitySuiteName, Suite{ diff --git a/internal/suites/suite_high_availability_test.go b/internal/suites/suite_high_availability_test.go index b78a4c884..26ccd56ef 100644 --- a/internal/suites/suite_high_availability_test.go +++ b/internal/suites/suite_high_availability_test.go @@ -1,18 +1,206 @@ package suites import ( + "context" + "fmt" + "net/http" + "strings" "testing" + "time" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -type HighAvailabilitySuite struct { +type HighAvailabilityWebDriverSuite struct { *SeleniumSuite } +func NewHighAvailabilityWebDriverSuite() *HighAvailabilityWebDriverSuite { + return &HighAvailabilityWebDriverSuite{SeleniumSuite: new(SeleniumSuite)} +} + +func (s *HighAvailabilityWebDriverSuite) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *HighAvailabilityWebDriverSuite) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserDataInDB() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password") + + err := haDockerEnvironment.Restart("mariadb") + s.Assert().NoError(err) + + time.Sleep(2 * time.Second) + + s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "") + s.verifyIsSecondFactorPage(ctx, s.T()) +} + +func (s *HighAvailabilityWebDriverSuite) TestShouldKeepSessionAfterAutheliaRestart() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + secret := s.doRegisterAndLogin2FA(ctx, s.T(), "john", "password", false, "") + + err := haDockerEnvironment.Restart("authelia-backend") + s.Assert().NoError(err) + + loop := true + for loop { + logs, err := haDockerEnvironment.Logs("authelia-backend", []string{"--tail", "10"}) + s.Assert().NoError(err) + + select { + case <-time.After(1 * time.Second): + if strings.Contains(logs, "Authelia is listening on :9091") { + loop = false + } + break + case <-ctx.Done(): + loop = false + break + } + } + + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) + + // Verify the user is still authenticated + s.doVisit(s.T(), LoginBaseURL) + s.verifyIsSecondFactorPage(ctx, s.T()) + + // Then logout and login again to check the secret is still there + s.doLogout(ctx, s.T()) + s.verifyIsFirstFactorPage(ctx, s.T()) + + s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, fmt.Sprintf("%s/secret.html", SecureBaseURL)) + s.verifySecretAuthorized(ctx, s.T()) +} + +var UserJohn = "john" +var UserBob = "bob" +var UserHarry = "harry" + +var Users = []string{UserJohn, UserBob, UserHarry} + +var expectedAuthorizations = map[string](map[string]bool){ + fmt.Sprintf("%s/secret.html", PublicBaseURL): map[string]bool{ + UserJohn: true, UserBob: true, UserHarry: true, + }, + fmt.Sprintf("%s/secret.html", SecureBaseURL): map[string]bool{ + UserJohn: true, UserBob: true, UserHarry: true, + }, + fmt.Sprintf("%s/secret.html", AdminBaseURL): map[string]bool{ + UserJohn: true, UserBob: false, UserHarry: false, + }, + fmt.Sprintf("%s/secret.html", SingleFactorBaseURL): map[string]bool{ + UserJohn: true, UserBob: true, UserHarry: true, + }, + fmt.Sprintf("%s/secret.html", MX1MailBaseURL): map[string]bool{ + UserJohn: true, UserBob: true, UserHarry: false, + }, + fmt.Sprintf("%s/secret.html", MX2MailBaseURL): map[string]bool{ + UserJohn: false, UserBob: true, UserHarry: false, + }, + + fmt.Sprintf("%s/groups/admin/secret.html", DevBaseURL): map[string]bool{ + UserJohn: true, UserBob: false, UserHarry: false, + }, + fmt.Sprintf("%s/groups/dev/secret.html", DevBaseURL): map[string]bool{ + UserJohn: true, UserBob: true, UserHarry: false, + }, + fmt.Sprintf("%s/users/john/secret.html", DevBaseURL): map[string]bool{ + UserJohn: true, UserBob: false, UserHarry: false, + }, + fmt.Sprintf("%s/users/harry/secret.html", DevBaseURL): map[string]bool{ + UserJohn: true, UserBob: false, UserHarry: true, + }, + fmt.Sprintf("%s/users/bob/secret.html", DevBaseURL): map[string]bool{ + UserJohn: true, UserBob: true, UserHarry: false, + }, +} + +func (s *HighAvailabilityWebDriverSuite) TestShouldVerifyAccessControl() { + verifyUserIsAuthorized := func(ctx context.Context, t *testing.T, username, targetURL string, authorized bool) { + s.doVisit(t, targetURL) + s.verifyURLIs(ctx, t, targetURL) + if authorized { + s.verifySecretAuthorized(ctx, t) + } else { + s.verifyBodyContains(ctx, t, "403 Forbidden") + } + } + + verifyAuthorization := func(username string) func(t *testing.T) { + return func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + s.doRegisterAndLogin2FA(ctx, t, username, "password", false, "") + + for url, authorizations := range expectedAuthorizations { + verifyUserIsAuthorized(ctx, t, username, url, authorizations[username]) + } + + s.doLogout(ctx, t) + } + } + + for _, user := range []string{UserJohn, UserBob, UserHarry} { + s.T().Run(fmt.Sprintf("user %s", user), verifyAuthorization(user)) + } +} + +type HighAvailabilitySuite struct { + suite.Suite +} + func NewHighAvailabilitySuite() *HighAvailabilitySuite { - return &HighAvailabilitySuite{SeleniumSuite: new(SeleniumSuite)} + return &HighAvailabilitySuite{} +} + +func DoGetWithAuth(t *testing.T, username, password string) int { + client := NewHTTPClient() + req, err := http.NewRequest("GET", fmt.Sprintf("%s/secret.html", SingleFactorBaseURL), nil) + req.SetBasicAuth(username, password) + assert.NoError(t, err) + + res, err := client.Do(req) + assert.NoError(t, err) + return res.StatusCode +} + +func (s *HighAvailabilitySuite) TestBasicAuth() { + s.Assert().Equal(DoGetWithAuth(s.T(), "john", "password"), 200) + s.Assert().Equal(DoGetWithAuth(s.T(), "john", "bad-password"), 302) + s.Assert().Equal(DoGetWithAuth(s.T(), "dontexist", "password"), 302) + } func TestHighAvailabilitySuite(t *testing.T) { - RunTypescriptSuite(t, highAvailabilitySuiteName) - TestRunOneFactor(t) + suite.Run(t, NewOneFactorScenario()) + suite.Run(t, NewTwoFactorScenario()) + suite.Run(t, NewRegulationScenario()) + suite.Run(t, NewCustomHeadersScenario()) + suite.Run(t, NewRedirectionCheckScenario()) + suite.Run(t, NewHighAvailabilityWebDriverSuite()) + suite.Run(t, NewHighAvailabilitySuite()) } diff --git a/internal/suites/suite_kubernetes_test.go b/internal/suites/suite_kubernetes_test.go index 879cd74ce..f41679c58 100644 --- a/internal/suites/suite_kubernetes_test.go +++ b/internal/suites/suite_kubernetes_test.go @@ -15,6 +15,6 @@ func NewKubernetesSuite() *KubernetesSuite { } func TestKubernetesSuite(t *testing.T) { - suite.Run(t, NewOneFactorSuite()) - suite.Run(t, NewTwoFactorSuite()) + suite.Run(t, NewOneFactorScenario()) + suite.Run(t, NewTwoFactorScenario()) } diff --git a/internal/suites/suite_ldap.go b/internal/suites/suite_ldap.go index 14849c29d..b42422669 100644 --- a/internal/suites/suite_ldap.go +++ b/internal/suites/suite_ldap.go @@ -9,6 +9,7 @@ var ldapSuiteName = "LDAP" func init() { dockerEnvironment := NewDockerEnvironment([]string{ "docker-compose.yml", + "internal/suites/LDAP/docker-compose.yml", "example/compose/authelia/docker-compose.backend.yml", "example/compose/authelia/docker-compose.frontend.yml", "example/compose/nginx/backend/docker-compose.yml", @@ -18,7 +19,7 @@ func init() { }) setup := func(suitePath string) error { - err := dockerEnvironment.Up(suitePath) + err := dockerEnvironment.Up() if err != nil { return err @@ -28,7 +29,7 @@ func init() { } teardown := func(suitePath string) error { - err := dockerEnvironment.Down(suitePath) + err := dockerEnvironment.Down() return err } diff --git a/internal/suites/suite_ldap_test.go b/internal/suites/suite_ldap_test.go index 9df63308f..521dcf4ec 100644 --- a/internal/suites/suite_ldap_test.go +++ b/internal/suites/suite_ldap_test.go @@ -15,6 +15,6 @@ func NewLDAPSuite() *LDAPSuite { } func TestLDAPSuite(t *testing.T) { - suite.Run(t, NewOneFactorSuite()) - suite.Run(t, NewTwoFactorSuite()) + suite.Run(t, NewOneFactorScenario()) + suite.Run(t, NewTwoFactorScenario()) } diff --git a/internal/suites/suite_mariadb.go b/internal/suites/suite_mariadb.go index 0d3c94f27..008ae148e 100644 --- a/internal/suites/suite_mariadb.go +++ b/internal/suites/suite_mariadb.go @@ -9,6 +9,7 @@ var mariadbSuiteName = "Mariadb" func init() { dockerEnvironment := NewDockerEnvironment([]string{ "docker-compose.yml", + "internal/suites/Mariadb/docker-compose.yml", "example/compose/authelia/docker-compose.backend.yml", "example/compose/authelia/docker-compose.frontend.yml", "example/compose/nginx/backend/docker-compose.yml", @@ -19,7 +20,7 @@ func init() { }) setup := func(suitePath string) error { - if err := dockerEnvironment.Up(suitePath); err != nil { + if err := dockerEnvironment.Up(); err != nil { return err } @@ -27,7 +28,7 @@ func init() { } teardown := func(suitePath string) error { - err := dockerEnvironment.Down(suitePath) + err := dockerEnvironment.Down() return err } diff --git a/internal/suites/suite_mariadb_test.go b/internal/suites/suite_mariadb_test.go index 66fe6eed0..c114bb00d 100644 --- a/internal/suites/suite_mariadb_test.go +++ b/internal/suites/suite_mariadb_test.go @@ -15,6 +15,6 @@ func NewMariadbSuite() *MariadbSuite { } func TestMariadbSuite(t *testing.T) { - suite.Run(t, NewOneFactorSuite()) - suite.Run(t, NewTwoFactorSuite()) + suite.Run(t, NewOneFactorScenario()) + suite.Run(t, NewTwoFactorScenario()) } diff --git a/internal/suites/suite_network_acl.go b/internal/suites/suite_network_acl.go index de91871a5..2fafbeeb6 100644 --- a/internal/suites/suite_network_acl.go +++ b/internal/suites/suite_network_acl.go @@ -9,6 +9,7 @@ var networkACLSuiteName = "NetworkACL" func init() { dockerEnvironment := NewDockerEnvironment([]string{ "docker-compose.yml", + "internal/suites/NetworkACL/docker-compose.yml", "example/compose/authelia/docker-compose.backend.yml", "example/compose/authelia/docker-compose.frontend.yml", "example/compose/nginx/backend/docker-compose.yml", @@ -20,7 +21,7 @@ func init() { }) setup := func(suitePath string) error { - if err := dockerEnvironment.Up(suitePath); err != nil { + if err := dockerEnvironment.Up(); err != nil { return err } @@ -28,7 +29,7 @@ func init() { } teardown := func(suitePath string) error { - return dockerEnvironment.Down(suitePath) + return dockerEnvironment.Down() } GlobalRegistry.Register(networkACLSuiteName, Suite{ diff --git a/internal/suites/suite_network_acl_test.go b/internal/suites/suite_network_acl_test.go index d3341eca3..21a0ad1d0 100644 --- a/internal/suites/suite_network_acl_test.go +++ b/internal/suites/suite_network_acl_test.go @@ -1,17 +1,102 @@ package suites import ( + "context" + "fmt" + "log" "testing" + "time" + + "github.com/stretchr/testify/suite" ) type NetworkACLSuite struct { - *SeleniumSuite + suite.Suite + + clients []*WebDriverSession } func NewNetworkACLSuite() *NetworkACLSuite { - return &NetworkACLSuite{SeleniumSuite: new(SeleniumSuite)} + return &NetworkACLSuite{clients: make([]*WebDriverSession, 3)} +} + +func (s *NetworkACLSuite) createClient(idx int) { + wds, err := StartWebDriverWithProxy(fmt.Sprintf("http://proxy-client%d.example.com:3128", idx), 4444+idx) + + if err != nil { + log.Fatal(err) + } + + s.clients[idx] = wds +} + +func (s *NetworkACLSuite) teardownClient(idx int) { + if err := s.clients[idx].Stop(); err != nil { + log.Fatal(err) + } +} + +func (s *NetworkACLSuite) SetupSuite() { + wds, err := StartWebDriver() + if err != nil { + log.Fatal(err) + } + s.clients[0] = wds + + for i := 1; i <= 2; i++ { + s.createClient(i) + } +} + +func (s *NetworkACLSuite) TearDownSuite() { + if err := s.clients[0].Stop(); err != nil { + log.Fatal(err) + } + for i := 1; i <= 2; i++ { + s.teardownClient(i) + } +} + +func (s *NetworkACLSuite) TestShouldAccessSecretUpon2FA() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + targetURL := fmt.Sprintf("%s/secret.html", SecureBaseURL) + secret := s.clients[0].doRegisterThenLogout(ctx, s.T(), "john", "password") + + s.clients[0].doVisit(s.T(), targetURL) + s.clients[0].verifyIsFirstFactorPage(ctx, s.T()) + + s.clients[0].doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL) + s.clients[0].verifyIsSecondFactorPage(ctx, s.T()) + s.clients[0].doValidateTOTP(ctx, s.T(), secret) + + s.clients[0].verifySecretAuthorized(ctx, s.T()) +} + +// from network 192.168.240.201/32 +func (s *NetworkACLSuite) TestShouldAccessSecretUpon1FA() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + targetURL := fmt.Sprintf("%s/secret.html", SecureBaseURL) + s.clients[1].doVisit(s.T(), targetURL) + s.clients[1].verifyIsFirstFactorPage(ctx, s.T()) + + s.clients[1].doLoginOneFactor(ctx, s.T(), "john", "password", + false, fmt.Sprintf("%s/secret.html", SecureBaseURL)) + s.clients[1].verifySecretAuthorized(ctx, s.T()) +} + +// from network 192.168.240.202/32 +func (s *NetworkACLSuite) TestShouldAccessSecretUpon0FA() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + s.clients[2].doVisit(s.T(), fmt.Sprintf("%s/secret.html", SecureBaseURL)) + s.clients[2].verifySecretAuthorized(ctx, s.T()) } func TestNetworkACLSuite(t *testing.T) { - RunTypescriptSuite(t, networkACLSuiteName) + suite.Run(t, NewNetworkACLSuite()) } diff --git a/internal/suites/suite_postgres.go b/internal/suites/suite_postgres.go index 89688abc1..788895fdf 100644 --- a/internal/suites/suite_postgres.go +++ b/internal/suites/suite_postgres.go @@ -9,6 +9,7 @@ var postgresSuiteName = "Postgres" func init() { dockerEnvironment := NewDockerEnvironment([]string{ "docker-compose.yml", + "internal/suites/Postgres/docker-compose.yml", "example/compose/authelia/docker-compose.backend.yml", "example/compose/authelia/docker-compose.frontend.yml", "example/compose/nginx/backend/docker-compose.yml", @@ -19,7 +20,7 @@ func init() { }) setup := func(suitePath string) error { - if err := dockerEnvironment.Up(suitePath); err != nil { + if err := dockerEnvironment.Up(); err != nil { return err } @@ -27,7 +28,7 @@ func init() { } teardown := func(suitePath string) error { - err := dockerEnvironment.Down(suitePath) + err := dockerEnvironment.Down() return err } diff --git a/internal/suites/suite_postgres_test.go b/internal/suites/suite_postgres_test.go index 49aab7887..59af70cd0 100644 --- a/internal/suites/suite_postgres_test.go +++ b/internal/suites/suite_postgres_test.go @@ -15,6 +15,6 @@ func NewPostgresSuite() *PostgresSuite { } func TestPostgresSuite(t *testing.T) { - suite.Run(t, NewOneFactorSuite()) - suite.Run(t, NewTwoFactorSuite()) + suite.Run(t, NewOneFactorScenario()) + suite.Run(t, NewTwoFactorScenario()) } diff --git a/internal/suites/suite_short_timeouts.go b/internal/suites/suite_short_timeouts.go index f808b843d..6d15b0b69 100644 --- a/internal/suites/suite_short_timeouts.go +++ b/internal/suites/suite_short_timeouts.go @@ -9,6 +9,7 @@ var shortTimeoutsSuiteName = "ShortTimeouts" func init() { dockerEnvironment := NewDockerEnvironment([]string{ "docker-compose.yml", + "internal/suites/ShortTimeouts/docker-compose.yml", "example/compose/authelia/docker-compose.backend.yml", "example/compose/authelia/docker-compose.frontend.yml", "example/compose/nginx/backend/docker-compose.yml", @@ -17,7 +18,7 @@ func init() { }) setup := func(suitePath string) error { - if err := dockerEnvironment.Up(suitePath); err != nil { + if err := dockerEnvironment.Up(); err != nil { return err } @@ -25,7 +26,7 @@ func init() { } teardown := func(suitePath string) error { - return dockerEnvironment.Down(suitePath) + return dockerEnvironment.Down() } GlobalRegistry.Register(shortTimeoutsSuiteName, Suite{ diff --git a/internal/suites/suite_short_timeouts_test.go b/internal/suites/suite_short_timeouts_test.go index 487a66e63..58f51f4e8 100644 --- a/internal/suites/suite_short_timeouts_test.go +++ b/internal/suites/suite_short_timeouts_test.go @@ -2,6 +2,8 @@ package suites import ( "testing" + + "github.com/stretchr/testify/suite" ) type ShortTimeoutsSuite struct { @@ -13,5 +15,6 @@ func NewShortTimeoutsSuite() *ShortTimeoutsSuite { } func TestShortTimeoutsSuite(t *testing.T) { - RunTypescriptSuite(t, shortTimeoutsSuiteName) + suite.Run(t, NewInactivityScenario()) + suite.Run(t, NewRegulationScenario()) } diff --git a/internal/suites/suite_standalone.go b/internal/suites/suite_standalone.go index 8bc339412..57544a1bb 100644 --- a/internal/suites/suite_standalone.go +++ b/internal/suites/suite_standalone.go @@ -9,6 +9,7 @@ var standaloneSuiteName = "Standalone" func init() { dockerEnvironment := NewDockerEnvironment([]string{ "docker-compose.yml", + "internal/suites/Standalone/docker-compose.yml", "example/compose/authelia/docker-compose.backend.yml", "example/compose/authelia/docker-compose.frontend.yml", "example/compose/nginx/backend/docker-compose.yml", @@ -17,7 +18,7 @@ func init() { }) setup := func(suitePath string) error { - err := dockerEnvironment.Up(suitePath) + err := dockerEnvironment.Up() if err != nil { return err @@ -27,7 +28,7 @@ func init() { } teardown := func(suitePath string) error { - err := dockerEnvironment.Down(suitePath) + err := dockerEnvironment.Down() return err } diff --git a/internal/suites/suite_standalone_test.go b/internal/suites/suite_standalone_test.go index 3d0327328..6629d538d 100644 --- a/internal/suites/suite_standalone_test.go +++ b/internal/suites/suite_standalone_test.go @@ -1,22 +1,138 @@ package suites import ( + "context" + "fmt" + "io/ioutil" + "log" + "net/http" "testing" + "time" "github.com/stretchr/testify/suite" ) -type StandaloneSuite struct { +type StandaloneWebDriverSuite struct { *SeleniumSuite } +func NewStandaloneWebDriverSuite() *StandaloneWebDriverSuite { + return &StandaloneWebDriverSuite{SeleniumSuite: new(SeleniumSuite)} +} + +func (s *StandaloneWebDriverSuite) SetupSuite() { + wds, err := StartWebDriver() + + if err != nil { + log.Fatal(err) + } + + s.WebDriverSession = wds +} + +func (s *StandaloneWebDriverSuite) TearDownSuite() { + err := s.WebDriverSession.Stop() + + if err != nil { + log.Fatal(err) + } +} + +func (s *StandaloneWebDriverSuite) SetupTest() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s.doLogout(ctx, s.T()) + s.WebDriverSession.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) +} + +func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + _ = s.doRegisterAndLogin2FA(ctx, s.T(), "john", "password", false, "") + + // Visit home page to change context + s.doVisit(s.T(), HomeBaseURL) + s.verifyIsHome(ctx, s.T()) + + // Visit the login page and wait for redirection to 2FA page with success icon displayed + s.doVisit(s.T(), LoginBaseURL) + s.verifyIsSecondFactorPage(ctx, s.T()) + + // Check whether the success icon is displayed + s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon") +} + +type StandaloneSuite struct { + suite.Suite +} + func NewStandaloneSuite() *StandaloneSuite { - return &StandaloneSuite{SeleniumSuite: new(SeleniumSuite)} + return &StandaloneSuite{} +} + +// Standard case using nginx +func (s *StandaloneSuite) TestShouldVerifyAPIVerifyUnauthorize() { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify", AutheliaBaseURL), nil) + s.Assert().NoError(err) + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Original-URL", AdminBaseURL) + + client := NewHTTPClient() + res, err := client.Do(req) + s.Assert().NoError(err) + s.Assert().Equal(res.StatusCode, 401) + body, err := ioutil.ReadAll(res.Body) + s.Assert().NoError(err) + s.Assert().Equal(string(body), "Unauthorized") +} + +// Standard case using Kubernetes +func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalURL() { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify?rd=%s", AutheliaBaseURL, LoginBaseURL), nil) + s.Assert().NoError(err) + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Original-URL", AdminBaseURL) + + client := NewHTTPClient() + res, err := client.Do(req) + s.Assert().NoError(err) + s.Assert().Equal(res.StatusCode, 302) + body, err := ioutil.ReadAll(res.Body) + s.Assert().NoError(err) + s.Assert().Equal(string(body), fmt.Sprintf("Found. Redirecting to %s?rd=%s", LoginBaseURL, AdminBaseURL)) +} + +func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI() { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify?rd=%s", AutheliaBaseURL, LoginBaseURL), nil) + s.Assert().NoError(err) + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-Host", "secure.example.com:8080") + req.Header.Set("X-Forwarded-URI", "/") + + client := NewHTTPClient() + res, err := client.Do(req) + s.Assert().NoError(err) + s.Assert().Equal(res.StatusCode, 302) + body, err := ioutil.ReadAll(res.Body) + s.Assert().NoError(err) + s.Assert().Equal(string(body), fmt.Sprintf("Found. Redirecting to %s?rd=https://secure.example.com:8080/", LoginBaseURL)) +} + +func TestStandaloneWebDriverScenario(t *testing.T) { + suite.Run(t, NewStandaloneWebDriverSuite()) } func TestStandaloneSuite(t *testing.T) { - suite.Run(t, NewOneFactorSuite()) - suite.Run(t, NewTwoFactorSuite()) + suite.Run(t, NewOneFactorScenario()) + suite.Run(t, NewTwoFactorScenario()) + suite.Run(t, NewBypassPolicyScenario()) + suite.Run(t, NewBackendProtectionScenario()) + suite.Run(t, NewResetPasswordScenario()) + suite.Run(t, NewAvailableMethodsScenario([]string{"ONE-TIME PASSWORD"})) - RunTypescriptSuite(t, standaloneSuiteName) + suite.Run(t, NewStandaloneWebDriverSuite()) + suite.Run(t, NewStandaloneSuite()) } diff --git a/internal/suites/suite_traefik.go b/internal/suites/suite_traefik.go index 75652835b..09ee55729 100644 --- a/internal/suites/suite_traefik.go +++ b/internal/suites/suite_traefik.go @@ -9,6 +9,7 @@ var traefikSuiteName = "Traefik" func init() { dockerEnvironment := NewDockerEnvironment([]string{ "docker-compose.yml", + "internal/suites/Traefik/docker-compose.yml", "example/compose/authelia/docker-compose.backend.yml", "example/compose/authelia/docker-compose.frontend.yml", "example/compose/nginx/backend/docker-compose.yml", @@ -17,7 +18,7 @@ func init() { }) setup := func(suitePath string) error { - err := dockerEnvironment.Up(suitePath) + err := dockerEnvironment.Up() if err != nil { return err @@ -27,7 +28,7 @@ func init() { } teardown := func(suitePath string) error { - err := dockerEnvironment.Down(suitePath) + err := dockerEnvironment.Down() return err } diff --git a/internal/suites/suite_traefik_test.go b/internal/suites/suite_traefik_test.go index 8d543fd58..0d87ab774 100644 --- a/internal/suites/suite_traefik_test.go +++ b/internal/suites/suite_traefik_test.go @@ -15,6 +15,6 @@ func NewTraefikSuite() *TraefikSuite { } func TestTraefikSuite(t *testing.T) { - suite.Run(t, NewOneFactorSuite()) - suite.Run(t, NewTwoFactorSuite()) + suite.Run(t, NewOneFactorScenario()) + suite.Run(t, NewTwoFactorScenario()) } diff --git a/internal/suites/suites.go b/internal/suites/suites.go index 373d4e83c..9fad174d2 100644 --- a/internal/suites/suites.go +++ b/internal/suites/suites.go @@ -1,14 +1,6 @@ package suites import ( - "context" - "errors" - "fmt" - "os" - "testing" - - "github.com/clems4ever/authelia/internal/utils" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/tebeka/selenium" ) @@ -24,55 +16,3 @@ type SeleniumSuite struct { func (s *SeleniumSuite) WebDriver() selenium.WebDriver { return s.WebDriverSession.WebDriver } - -// Wait wait until condition holds true -func (s *SeleniumSuite) Wait(ctx context.Context, condition selenium.Condition) error { - done := make(chan error, 1) - go func() { - done <- s.WebDriverSession.WebDriver.Wait(condition) - }() - - select { - case <-ctx.Done(): - return errors.New("waiting timeout reached") - case err := <-done: - return err - } -} - -func rootPath() string { - rootPath := os.Getenv("ROOT_PATH") - - // If env variable is not provided, use relative path. - if rootPath == "" { - rootPath = "../.." - } - return rootPath -} - -func relativePath(path string) string { - return fmt.Sprintf("%s/%s", rootPath(), path) -} - -// RunTypescriptSuite run the tests of the typescript suite -func RunTypescriptSuite(t *testing.T, suite string) { - forbidFlags := "" - if os.Getenv("ONLY_FORBIDDEN") == "true" { - forbidFlags = "--forbid-only --forbid-pending" - } - - cmdline := "./node_modules/.bin/mocha" + - " --exit --require ts-node/register " + forbidFlags + " " + - fmt.Sprintf("test/suites/%s/test.ts", suite) - - command := utils.CommandWithStdout("bash", "-c", cmdline) - command.Stdout = os.Stdout - command.Stderr = os.Stderr - command.Dir = rootPath() - command.Env = append( - os.Environ(), - "ENVIRONMENT=dev", - fmt.Sprintf("TS_NODE_PROJECT=%s", "test/tsconfig.json")) - - assert.NoError(t, command.Run()) -} diff --git a/internal/suites/verify_body_contains.go b/internal/suites/verify_body_contains.go index c91b44678..0975e52dd 100644 --- a/internal/suites/verify_body_contains.go +++ b/internal/suites/verify_body_contains.go @@ -1,8 +1,26 @@ package suites -import "context" +import ( + "context" + "strings" + "testing" -func verifyBodyContains(ctx context.Context, s *SeleniumSuite, pattern string) { - bodyElement := WaitElementLocatedByTagName(ctx, s, "body") - WaitElementTextContains(ctx, s, bodyElement, pattern) + "github.com/stretchr/testify/require" + "github.com/tebeka/selenium" +) + +func (wds *WebDriverSession) verifyBodyContains(ctx context.Context, t *testing.T, pattern string) { + err := wds.Wait(ctx, func(wd selenium.WebDriver) (bool, error) { + bodyElement := wds.WaitElementLocatedByTagName(ctx, t, "body") + require.NotNil(t, bodyElement) + + content, err := bodyElement.Text() + + if err != nil { + return false, err + } + + return strings.Contains(content, pattern), nil + }) + require.NoError(t, err) } diff --git a/internal/suites/verify_is_first_factor_page.go b/internal/suites/verify_is_first_factor_page.go index e4d1c2f7b..3afe409c8 100644 --- a/internal/suites/verify_is_first_factor_page.go +++ b/internal/suites/verify_is_first_factor_page.go @@ -1,7 +1,10 @@ package suites -import "context" +import ( + "context" + "testing" +) -func verifyIsFirstFactorPage(ctx context.Context, s *SeleniumSuite) { - WaitElementLocatedByClassName(ctx, s, "first-factor-step") +func (wds *WebDriverSession) verifyIsFirstFactorPage(ctx context.Context, t *testing.T) { + wds.WaitElementLocatedByID(ctx, t, "first-factor-stage") } diff --git a/internal/suites/verify_is_home.go b/internal/suites/verify_is_home.go new file mode 100644 index 000000000..c1bdedf01 --- /dev/null +++ b/internal/suites/verify_is_home.go @@ -0,0 +1,11 @@ +package suites + +import ( + "context" + "fmt" + "testing" +) + +func (wds *WebDriverSession) verifyIsHome(ctx context.Context, t *testing.T) { + wds.verifyURLIs(ctx, t, fmt.Sprintf("%s/", HomeBaseURL)) +} diff --git a/internal/suites/verify_is_second_factor_page.go b/internal/suites/verify_is_second_factor_page.go index 7953a0655..8cc5c3259 100644 --- a/internal/suites/verify_is_second_factor_page.go +++ b/internal/suites/verify_is_second_factor_page.go @@ -1,7 +1,10 @@ package suites -import "context" +import ( + "context" + "testing" +) -func verifyIsSecondFactorPage(ctx context.Context, s *SeleniumSuite) { - WaitElementLocatedByClassName(ctx, s, "second-factor-step") +func (wds *WebDriverSession) verifyIsSecondFactorPage(ctx context.Context, t *testing.T) { + wds.WaitElementLocatedByID(ctx, t, "second-factor-stage") } diff --git a/internal/suites/verify_mail.go b/internal/suites/verify_mail.go new file mode 100644 index 000000000..663594b8c --- /dev/null +++ b/internal/suites/verify_mail.go @@ -0,0 +1,10 @@ +package suites + +import ( + "context" + "testing" +) + +func (wds *WebDriverSession) verifyMailNotificationDisplayed(ctx context.Context, t *testing.T) { + wds.verifyNotificationDisplayed(ctx, t, "An email has been sent to your address to complete the process.") +} diff --git a/internal/suites/verify_notification.go b/internal/suites/verify_notification.go index 3db95b163..26dba4792 100644 --- a/internal/suites/verify_notification.go +++ b/internal/suites/verify_notification.go @@ -1,9 +1,14 @@ package suites -import "context" +import ( + "context" + "testing" -func verifyNotificationDisplayed(ctx context.Context, s *SeleniumSuite, message string) { - txt, err := WaitElementLocatedByClassName(ctx, s, "notification").Text() - s.Assert().NoError(err) - s.Assert().Equal(message, txt) + "github.com/stretchr/testify/assert" +) + +func (wds *WebDriverSession) verifyNotificationDisplayed(ctx context.Context, t *testing.T, message string) { + el := wds.WaitElementLocatedByClassName(ctx, t, "notification") + assert.NotNil(t, el) + wds.WaitElementTextContains(ctx, t, el, message) } diff --git a/internal/suites/verify_secret_authorized.go b/internal/suites/verify_secret_authorized.go index 1dad98d5b..dc68199e8 100644 --- a/internal/suites/verify_secret_authorized.go +++ b/internal/suites/verify_secret_authorized.go @@ -1,7 +1,10 @@ package suites -import "context" +import ( + "context" + "testing" +) -func verifySecretAuthorized(ctx context.Context, s *SeleniumSuite) { - verifyBodyContains(ctx, s, "This is a very important secret!") +func (wds *WebDriverSession) verifySecretAuthorized(ctx context.Context, t *testing.T) { + wds.verifyBodyContains(ctx, t, "This is a very important secret!") } diff --git a/internal/suites/verify_url_is.go b/internal/suites/verify_url_is.go index 6a2ed4421..b90236dee 100644 --- a/internal/suites/verify_url_is.go +++ b/internal/suites/verify_url_is.go @@ -2,21 +2,21 @@ package suites import ( "context" + "testing" "github.com/stretchr/testify/assert" "github.com/tebeka/selenium" ) -func verifyURLIs(ctx context.Context, s *SeleniumSuite, url string) { - err := s.Wait(ctx, func(driver selenium.WebDriver) (bool, error) { +func (wds *WebDriverSession) verifyURLIs(ctx context.Context, t *testing.T, url string) { + err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) { currentURL, err := driver.CurrentURL() if err != nil { return false, err } - return currentURL == url, nil }) - assert.NoError(s.T(), err) + assert.NoError(t, err) } diff --git a/internal/suites/webdriver.go b/internal/suites/webdriver.go index 9e968dec1..fb25d5be7 100644 --- a/internal/suites/webdriver.go +++ b/internal/suites/webdriver.go @@ -2,11 +2,13 @@ package suites import ( "context" + "errors" "fmt" "os" "strings" + "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tebeka/selenium" "github.com/tebeka/selenium/chrome" ) @@ -17,9 +19,8 @@ type WebDriverSession struct { WebDriver selenium.WebDriver } -// StartWebDriver create a selenium session -func StartWebDriver() (*WebDriverSession, error) { - port := 4444 +// StartWebDriverWithProxy create a selenium session +func StartWebDriverWithProxy(proxy string, port int) (*WebDriverSession, error) { service, err := selenium.NewChromeDriverService("/usr/bin/chromedriver", port) if err != nil { @@ -34,6 +35,10 @@ func StartWebDriver() (*WebDriverSession, error) { chromeCaps.Args = append(chromeCaps.Args, "--headless") } + if proxy != "" { + chromeCaps.Args = append(chromeCaps.Args, fmt.Sprintf("--proxy-server=%s", proxy)) + } + caps := selenium.Capabilities{} caps.AddChrome(chromeCaps) @@ -49,6 +54,11 @@ func StartWebDriver() (*WebDriverSession, error) { }, nil } +// StartWebDriver create a selenium session +func StartWebDriver() (*WebDriverSession, error) { + return StartWebDriverWithProxy("", 4444) +} + // Stop stop the selenium session func (wds *WebDriverSession) Stop() error { err := wds.WebDriver.Quit() @@ -73,9 +83,24 @@ func WithWebdriver(fn func(webdriver selenium.WebDriver) error) error { return fn(wds.WebDriver) } -func waitElementLocated(ctx context.Context, s *SeleniumSuite, by, value string) selenium.WebElement { +// Wait wait until condition holds true +func (wds *WebDriverSession) Wait(ctx context.Context, condition selenium.Condition) error { + done := make(chan error, 1) + go func() { + done <- wds.WebDriver.Wait(condition) + }() + + select { + case <-ctx.Done(): + return errors.New("waiting timeout reached") + case err := <-done: + return err + } +} + +func (wds *WebDriverSession) waitElementLocated(ctx context.Context, t *testing.T, by, value string) selenium.WebElement { var el selenium.WebElement - err := s.Wait(ctx, func(driver selenium.WebDriver) (bool, error) { + err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) { var err error el, err = driver.FindElement(by, value) @@ -89,31 +114,65 @@ func waitElementLocated(ctx context.Context, s *SeleniumSuite, by, value string) return el != nil, nil }) - assert.NoError(s.T(), err) - assert.NotNil(s.T(), el, "Element has not been located") + require.NoError(t, err) + require.NotNil(t, el) + return el +} + +func (wds *WebDriverSession) waitElementsLocated(ctx context.Context, t *testing.T, by, value string) []selenium.WebElement { + var el []selenium.WebElement + err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) { + var err error + el, err = driver.FindElements(by, value) + + if err != nil { + if strings.Contains(err.Error(), "no such element") { + return false, nil + } + return false, err + } + + return el != nil, nil + }) + + require.NoError(t, err) + require.NotNil(t, el) return el } // WaitElementLocatedByID wait an element is located by id -func WaitElementLocatedByID(ctx context.Context, s *SeleniumSuite, id string) selenium.WebElement { - return waitElementLocated(ctx, s, selenium.ByID, id) +func (wds *WebDriverSession) WaitElementLocatedByID(ctx context.Context, t *testing.T, id string) selenium.WebElement { + return wds.waitElementLocated(ctx, t, selenium.ByID, id) } // WaitElementLocatedByTagName wait an element is located by tag name -func WaitElementLocatedByTagName(ctx context.Context, s *SeleniumSuite, tagName string) selenium.WebElement { - return waitElementLocated(ctx, s, selenium.ByTagName, tagName) +func (wds *WebDriverSession) WaitElementLocatedByTagName(ctx context.Context, t *testing.T, tagName string) selenium.WebElement { + return wds.waitElementLocated(ctx, t, selenium.ByTagName, tagName) } // WaitElementLocatedByClassName wait an element is located by class name -func WaitElementLocatedByClassName(ctx context.Context, s *SeleniumSuite, className string) selenium.WebElement { - return waitElementLocated(ctx, s, selenium.ByClassName, className) +func (wds *WebDriverSession) WaitElementLocatedByClassName(ctx context.Context, t *testing.T, className string) selenium.WebElement { + return wds.waitElementLocated(ctx, t, selenium.ByClassName, className) +} + +// WaitElementLocatedByLinkText wait an element is located by link text +func (wds *WebDriverSession) WaitElementLocatedByLinkText(ctx context.Context, t *testing.T, linkText string) selenium.WebElement { + return wds.waitElementLocated(ctx, t, selenium.ByLinkText, linkText) +} + +// WaitElementLocatedByCSSSelector wait an element is located by class name +func (wds *WebDriverSession) WaitElementLocatedByCSSSelector(ctx context.Context, t *testing.T, cssSelector string) selenium.WebElement { + return wds.waitElementLocated(ctx, t, selenium.ByCSSSelector, cssSelector) +} + +// WaitElementsLocatedByCSSSelector wait an element is located by CSS selector +func (wds *WebDriverSession) WaitElementsLocatedByCSSSelector(ctx context.Context, t *testing.T, cssSelector string) []selenium.WebElement { + return wds.waitElementsLocated(ctx, t, selenium.ByCSSSelector, cssSelector) } // WaitElementTextContains wait the text of an element contains a pattern -func WaitElementTextContains(ctx context.Context, s *SeleniumSuite, element selenium.WebElement, pattern string) { - assert.NotNil(s.T(), element) - - s.Wait(ctx, func(driver selenium.WebDriver) (bool, error) { +func (wds *WebDriverSession) WaitElementTextContains(ctx context.Context, t *testing.T, element selenium.WebElement, pattern string) { + err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) { text, err := element.Text() if err != nil { @@ -122,4 +181,5 @@ func WaitElementTextContains(ctx context.Context, s *SeleniumSuite, element sele return strings.Contains(text, pattern), nil }) + require.NoError(t, err) } diff --git a/internal/utils/clock.go b/internal/utils/clock.go new file mode 100644 index 000000000..94ddd7056 --- /dev/null +++ b/internal/utils/clock.go @@ -0,0 +1,22 @@ +package utils + +import "time" + +// Clock is an interface for a clock +type Clock interface { + Now() time.Time + After(d time.Duration) <-chan time.Time +} + +// RealClock is the implementation of a clock for production code +type RealClock struct{} + +// Now return the current time +func (RealClock) Now() time.Time { + return time.Now() +} + +// After return a channel receiving the time after the defined duration +func (RealClock) After(d time.Duration) <-chan time.Time { + return time.After(d) +} diff --git a/internal/utils/exec.go b/internal/utils/exec.go index d228d026e..c5d9691a5 100644 --- a/internal/utils/exec.go +++ b/internal/utils/exec.go @@ -6,6 +6,8 @@ import ( "os" "os/exec" "os/signal" + "path/filepath" + "strings" "sync" "syscall" "time" @@ -13,9 +15,22 @@ import ( log "github.com/sirupsen/logrus" ) +// Command create a command at the project root +func Command(name string, args ...string) *exec.Cmd { + cmd := exec.Command(name, args...) + + // By default set the working directory to the project root directory + wd, _ := os.Getwd() + for !strings.HasSuffix(wd, "authelia") { + wd = filepath.Dir(wd) + } + cmd.Dir = wd + return cmd +} + // CommandWithStdout create a command forwarding stdout and stderr to the OS streams func CommandWithStdout(name string, args ...string) *exec.Cmd { - cmd := exec.Command(name, args...) + cmd := Command(name, args...) if log.GetLevel() > log.InfoLevel { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index f9f404e89..000000000 --- a/package-lock.json +++ /dev/null @@ -1,3915 +0,0 @@ -{ - "name": "authelia", - "version": "3.16.3", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@types/bluebird": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.27.tgz", - "integrity": "sha512-6BmYWSBea18+tSjjSC3QIyV93ZKAeNWGM7R6aYt1ryTZXrlHF+QLV0G2yV0viEGVyRkyQsWfMoJ0k/YghBX5sQ==", - "dev": true - }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", - "dev": true - }, - "@types/chokidar": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/chokidar/-/chokidar-1.7.5.tgz", - "integrity": "sha512-PDkSRY7KltW3M60hSBlerxI8SFPXsO3AL/aRVsO4Kh9IHRW74Ih75gUuTd/aE4LSSFqypb10UIX3QzOJwBQMGQ==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/node": "*" - } - }, - "@types/commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==", - "dev": true, - "requires": { - "commander": "*" - } - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, - "@types/form-data": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/mocha": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", - "dev": true - }, - "@types/mockdate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mockdate/-/mockdate-2.0.0.tgz", - "integrity": "sha1-qvOIoerTsPXtbcFhGVbqe0ClfTw=", - "dev": true - }, - "@types/node": { - "version": "12.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz", - "integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==", - "dev": true - }, - "@types/node-fetch": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.3.7.tgz", - "integrity": "sha512-+bKtuxhj/TYSSP1r4CZhfmyA0vm/aDRQNo7vbAgf6/cZajn0SAniGGST07yvI4Q+q169WTa2/x9gEHfJrkcALw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/query-string": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/query-string/-/query-string-5.1.0.tgz", - "integrity": "sha512-9/sJK+T04pNq7uwReR0CLxqXj1dhxiTapZ1tIxA0trEsT6FRS0bz09YMcMb7tsVBTm4RJ0NEBYGsAjoEmqoFXg==", - "dev": true - }, - "@types/redis": { - "version": "2.8.14", - "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.14.tgz", - "integrity": "sha512-255dzsOLJdXFHBio9/aMHGozNkoiBUgc+g2nlNjbTSp5qcAlmpm4Z6Xs3pKOBLNIKdZbA2BkUxWvYSIwKra0Yw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/request": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", - "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", - "dev": true, - "requires": { - "@types/caseless": "*", - "@types/form-data": "*", - "@types/node": "*", - "@types/tough-cookie": "*" - } - }, - "@types/request-promise": { - "version": "4.1.44", - "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.44.tgz", - "integrity": "sha512-RId7eFsUKxfal1LirDDIcOp9u3MM3NXFDBcC3sqIMcmu7f4U6DsCEMD8RbLZtnPrQlN5Jc79di/WPsIEDO4keg==", - "dev": true, - "requires": { - "@types/bluebird": "*", - "@types/request": "*" - } - }, - "@types/selenium-webdriver": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.16.tgz", - "integrity": "sha512-lMC2G0ItF2xv4UCiwbJGbnJlIuUixHrioOhNGHSCsYCJ8l4t9hMCUimCytvFv7qy6AfSzRxhRHoGa+UqaqwyeA==", - "dev": true - }, - "@types/speakeasy": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/speakeasy/-/speakeasy-2.0.4.tgz", - "integrity": "sha512-WcZalHN3tlh+StC8cszTuh2SkX+vn5s4K+eMwa2fXM4t3GDeYg6JVrpchHs9InqTkgXXsEtE8KNXaQxfkIdmng==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/tough-cookie": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", - "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", - "dev": true - }, - "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base32.js": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", - "integrity": "sha1-0EVzalex9sE58MffQlGKhOkbsro=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "bluebird": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "chromedriver": { - "version": "77.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-77.0.0.tgz", - "integrity": "sha512-mZa1IVx4HD8rDaItWbnS470mmypgiWsDiu98r0NkiT4uLm3qrANl4vOU6no6vtWtLQiW5kt1POcIbjeNpsLbXA==", - "dev": true, - "requires": { - "del": "^4.1.1", - "extract-zip": "^1.6.7", - "mkdirp": "^0.5.1", - "request": "^2.88.0", - "tcp-port-used": "^1.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "double-ended-queue": { - "version": "2.1.0-0", - "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", - "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=", - "dev": true - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ejs": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.2.tgz", - "integrity": "sha512-PcW2a0tyTuPHz3tWyYqtK6r1fZ3gp+3Sop8Ph+ZYN81Ob5rwmbHEzaqs10N3BEsaGTkh/ooniXK+WwszGlc2+Q==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extract-zip": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", - "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", - "dev": true, - "requires": { - "concat-stream": "1.6.2", - "debug": "2.6.9", - "mkdirp": "0.5.1", - "yauzl": "2.4.1" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", - "dev": true - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", - "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "requires": { - "is-path-inside": "^2.1.0" - }, - "dependencies": { - "is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "requires": { - "path-is-inside": "^1.0.2" - } - } - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", - "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "ip-regex": "^2.1.0", - "is-url": "^1.2.2" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jszip": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.1.tgz", - "integrity": "sha512-iCMBbo4eE5rb1VCpm5qXOAaUiRKRUKiItn8ah2YQQx9qymmSAY98eyQfioChEYcVQLh0zxJ3wS4A0mh90AVPvw==", - "dev": true, - "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "requires": { - "immediate": "~3.0.5" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - }, - "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", - "dev": true - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", - "dev": true, - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.5", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - } - }, - "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "mockdate": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-2.0.3.tgz", - "integrity": "sha512-/wRyr3grWk3tyk188qjZpeiiAfkAoDPEGqyerretomeeaH0D+pN9MCcedhAwrkxX3a216gp8CwPeQMHfLvrFpA==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "psl": { - "version": "1.1.33", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.33.tgz", - "integrity": "sha512-LTDP2uSrsc7XCb5lO7A8BI1qYxRe/8EqlRvMeEl6rsnYAqDOl8xHR+8lSAIVfrNaSAlTPTNOCgNjWcoUL3AZsw==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "query-string": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.8.1.tgz", - "integrity": "sha512-g6y0Lbq10a5pPQpjlFuojfMfV1Pd2Jw9h75ypiYPPia3Gcq2rgkKiIwbkS6JxH7c5f5u/B/sB+d13PU+g1eu4Q==", - "dev": true, - "requires": { - "decode-uri-component": "^0.2.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "redis": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", - "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", - "dev": true, - "requires": { - "double-ended-queue": "^2.1.0-0", - "redis-commands": "^1.2.0", - "redis-parser": "^2.6.0" - } - }, - "redis-commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", - "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==", - "dev": true - }, - "redis-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", - "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=", - "dev": true - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "request-promise": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz", - "integrity": "sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg==", - "dev": true, - "requires": { - "bluebird": "^3.5.0", - "request-promise-core": "1.1.2", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "resolve": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "selenium-webdriver": { - "version": "4.0.0-alpha.4", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.4.tgz", - "integrity": "sha512-etJt20d8qInkxMAHIm5SEpPBSS+CdxVcybnxzSIB/GlWErb8pIWrArz/VA6VfUW0/6tIcokepXQ5ufvdzqqk1A==", - "dev": true, - "requires": { - "jszip": "^3.1.5", - "rimraf": "^2.6.3", - "tmp": "0.0.30", - "xml2js": "^0.4.19" - }, - "dependencies": { - "tmp": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", - "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.1" - } - } - } - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "speakeasy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", - "integrity": "sha1-hckaBxsJpcuGQlkNmDVmFl9XYTo=", - "dev": true, - "requires": { - "base32.js": "0.0.1" - } - }, - "split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "tcp-port-used": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz", - "integrity": "sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==", - "dev": true, - "requires": { - "debug": "4.1.0", - "is2": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", - "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "tree-kill": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", - "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", - "dev": true - }, - "ts-node": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.2.0.tgz", - "integrity": "sha512-ZNT+OEGfUNVMGkpIaDJJ44Zq3Yr0bkU/ugN1PHbU+/01Z7UV1fsELRiTx1KuQNvQ1A3pGh3y25iYF6jXgxV21A==", - "dev": true, - "requires": { - "arrify": "^1.0.0", - "buffer-from": "^1.1.0", - "diff": "^3.1.0", - "make-error": "^1.1.1", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "source-map-support": "^0.5.6", - "yn": "^2.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true - }, - "tslint": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.18.0.tgz", - "integrity": "sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^3.2.0", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.29.0" - } - }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "dev": true, - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", - "dev": true - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", - "dev": true, - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - } - }, - "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", - "dev": true, - "requires": { - "fd-slicer": "~1.0.1" - } - }, - "yn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", - "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", - "dev": true - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 148c8071d..000000000 --- a/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "authelia", - "version": "3.16.3", - "description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F", - "engines": { - "node": ">=8.0.0 <10.0.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/clems4ever/authelia" - }, - "author": "Clement Michaud ", - "license": "MIT", - "bugs": { - "url": "https://github.com/clems4ever/authelia/issues" - }, - "apidoc": { - "title": "Authelia API documentation" - }, - "dependencies": {}, - "devDependencies": { - "@types/mocha": "^5.2.6", - "@types/node-fetch": "^2.1.4", - "@types/query-string": "^5.1.0", - "@types/request": "^2.0.5", - "@types/request-promise": "^4.1.38", - "@types/selenium-webdriver": "^3.0.16", - "@types/speakeasy": "^2.0.2", - "chromedriver": "^77.0.0", - "ejs": "^2.6.2", - "mocha": "^6.1.4", - "node-fetch": "^2.3.0", - "query-string": "^6.0.0", - "readable-stream": "^2.3.3", - "request": "^2.88.0", - "request-promise": "^4.2.2", - "selenium-webdriver": "^4.0.0-alpha.4", - "speakeasy": "^2.0.0", - "ts-node": "^6.0.1", - "tslint": "^5.2.0", - "typescript": "^2.9.2" - } -} diff --git a/scripts/setup-environment.ts b/scripts/setup-environment.ts deleted file mode 100644 index 4f94510bf..000000000 --- a/scripts/setup-environment.ts +++ /dev/null @@ -1,10 +0,0 @@ -var { setup } = require(`../test/suites/${process.argv[2]}/environment`); - -(async function() { - try { - await setup(); - } catch(err) { - console.error(err); - process.exit(1); - } -})() diff --git a/scripts/teardown-environment.ts b/scripts/teardown-environment.ts deleted file mode 100644 index 0f74c2d01..000000000 --- a/scripts/teardown-environment.ts +++ /dev/null @@ -1,10 +0,0 @@ -var { teardown } = require(`../test/suites/${process.argv[2]}/environment`); - -(async function() { - try { - await teardown(); - } catch(err) { - console.error(err); - process.exit(1); - } -})() diff --git a/scripts/utils/ListSuites.js b/scripts/utils/ListSuites.js deleted file mode 100644 index 546d05f8e..000000000 --- a/scripts/utils/ListSuites.js +++ /dev/null @@ -1,13 +0,0 @@ -const { lstatSync, readdirSync } = require('fs') -const { join } = require('path') - -const isDirectory = source => lstatSync(source).isDirectory() -const getDirectories = source => - readdirSync(source) - .map(name => join(source, name)) - .filter(isDirectory) - .map(x => x.split('/').slice(-1)[0]) - -module.exports = function() { - return getDirectories('test/suites/'); -} \ No newline at end of file diff --git a/scripts/utils/exec.js b/scripts/utils/exec.js deleted file mode 100644 index a8af70362..000000000 --- a/scripts/utils/exec.js +++ /dev/null @@ -1,18 +0,0 @@ -var spawn = require('child_process').spawn; - -function exec(cmd) { - return new Promise((resolve, reject) => { - const command = spawn(cmd, {shell: true, env: process.env}); - command.stdout.pipe(process.stdout); - command.stderr.pipe(process.stderr); - command.on('exit', function(statusCode) { - if (statusCode != 0) { - reject(new Error('Command \'' + cmd + '\' has exited with status ' + statusCode + '.')); - return; - } - resolve(); - }) - }) -} - -module.exports = { exec } \ No newline at end of file diff --git a/test/helpers/ClickOn.ts b/test/helpers/ClickOn.ts deleted file mode 100644 index 75b0b1afd..000000000 --- a/test/helpers/ClickOn.ts +++ /dev/null @@ -1,7 +0,0 @@ -import SeleniumWebdriver, { WebDriver, Locator } from "selenium-webdriver"; - -export default async function(driver: WebDriver, locator: Locator, timeout: number = 5000) { - const el = await driver.wait( - SeleniumWebdriver.until.elementLocated(locator), timeout); - await el.click(); -}; \ No newline at end of file diff --git a/test/helpers/ClickOnLink.ts b/test/helpers/ClickOnLink.ts deleted file mode 100644 index eee73bab0..000000000 --- a/test/helpers/ClickOnLink.ts +++ /dev/null @@ -1,8 +0,0 @@ -import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, linkText: string, timeout: number = 5000) { - const element = await driver.wait( - SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.linkText(linkText)), timeout); - await element.click(); -}; \ No newline at end of file diff --git a/test/helpers/FillField.ts b/test/helpers/FillField.ts deleted file mode 100644 index c02594745..000000000 --- a/test/helpers/FillField.ts +++ /dev/null @@ -1,9 +0,0 @@ -import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, fieldName: string, text: string, timeout: number = 5000) { - const element = await driver.wait( - SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.name(fieldName)), timeout) - - await element.sendKeys(text); -}; \ No newline at end of file diff --git a/test/helpers/FillLoginPageAndClick.ts b/test/helpers/FillLoginPageAndClick.ts deleted file mode 100644 index 276c322dd..000000000 --- a/test/helpers/FillLoginPageAndClick.ts +++ /dev/null @@ -1,17 +0,0 @@ -import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; - -export default async function( - driver: WebDriver, - username: string, - password: string, - keepMeLoggedIn: boolean = false, - timeout: number = 5000) { - - await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("username")), timeout) - await driver.findElement(SeleniumWebdriver.By.id("username")).sendKeys(username); - await driver.findElement(SeleniumWebdriver.By.id("password")).sendKeys(password); - if (keepMeLoggedIn) { - await driver.findElement(SeleniumWebdriver.By.id("remember-checkbox")).click(); - } - await driver.findElement(SeleniumWebdriver.By.tagName("button")).click(); -}; \ No newline at end of file diff --git a/test/helpers/FullLogin.ts b/test/helpers/FullLogin.ts deleted file mode 100644 index cd9cca54d..000000000 --- a/test/helpers/FullLogin.ts +++ /dev/null @@ -1,11 +0,0 @@ -import FillLoginPageAndClick from "./FillLoginPageAndClick"; -import ValidateTotp from "./ValidateTotp"; -import { WebDriver } from "selenium-webdriver"; -import VisitPageAndWaitUrlIs from "./behaviors/VisitPageAndWaitUrlIs"; - -// Validate the two factors! -export default async function(driver: WebDriver, user: string, secret: string, url: string, timeout: number = 5000) { - await VisitPageAndWaitUrlIs(driver, `https://login.example.com:8080/#/?rd=${url}`, timeout); - await FillLoginPageAndClick(driver, user, 'password', false, timeout); - await ValidateTotp(driver, secret, timeout); -} \ No newline at end of file diff --git a/test/helpers/GetIdentityLink.ts b/test/helpers/GetIdentityLink.ts deleted file mode 100644 index d55247a4e..000000000 --- a/test/helpers/GetIdentityLink.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Bluebird = require("bluebird"); -import Fs = require("fs"); -import Request = require("request-promise"); - -export async function GetLinkFromFile() { - const data = await Bluebird.promisify(Fs.readFile)( - "/tmp/authelia/notification.txt") - const regexp = new RegExp(/Link: (.+)/); - const match = regexp.exec(data.toLocaleString()); - if (match == null) { - throw new Error('No match'); - } - return match[1]; -}; - -export async function GetLinkFromEmail() { - const data = await Request({ - method: "GET", - uri: "https://mail.example.com:8080/messages", - json: true, - rejectUnauthorized: false, - }); - - const messageId = data[data.length - 1].id; - const data2 = await Request({ - method: "GET", - rejectUnauthorized: false, - uri: `https://mail.example.com:8080/messages/${messageId}.html`, - }); - - const regexp = new RegExp(/.*<\/a>/); - const match = regexp.exec(data2); - if (match == null) { - throw new Error('No match'); - } - return match[1]; -}; \ No newline at end of file diff --git a/test/helpers/LoginAndRegisterTotp.ts b/test/helpers/LoginAndRegisterTotp.ts deleted file mode 100644 index 5fa4326d4..000000000 --- a/test/helpers/LoginAndRegisterTotp.ts +++ /dev/null @@ -1,10 +0,0 @@ -import RegisterTotp from './RegisterTotp'; -import LoginAs from './LoginAs'; -import { WebDriver } from 'selenium-webdriver'; -import VerifyIsSecondFactorStage from './assertions/VerifyIsSecondFactorStage'; - -export default async function(driver: WebDriver, user: string, password: string, email: boolean = false, timeout: number = 5000) { - await LoginAs(driver, user, password, undefined, timeout); - await VerifyIsSecondFactorStage(driver, timeout); - return RegisterTotp(driver, email, timeout); -} \ No newline at end of file diff --git a/test/helpers/LoginAs.ts b/test/helpers/LoginAs.ts deleted file mode 100644 index ed57756c2..000000000 --- a/test/helpers/LoginAs.ts +++ /dev/null @@ -1,9 +0,0 @@ -import FillLoginPageAndClick from './FillLoginPageAndClick'; -import { WebDriver } from "selenium-webdriver"; -import VisitPageAndWaitUrlIs from "./behaviors/VisitPageAndWaitUrlIs"; - -export default async function(driver: WebDriver, user: string, password: string, targetUrl?: string, timeout: number = 5000) { - const urlExt = (targetUrl) ? ('rd=' + targetUrl) : ''; - await VisitPageAndWaitUrlIs(driver, "https://login.example.com:8080/#/?" + urlExt, timeout); - await FillLoginPageAndClick(driver, user, password, false, timeout); -} \ No newline at end of file diff --git a/test/helpers/Logout.ts b/test/helpers/Logout.ts deleted file mode 100644 index 32576b2c8..000000000 --- a/test/helpers/Logout.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { WebDriver } from "selenium-webdriver"; -import VerifyIsFirstFactorStage from "./assertions/VerifyIsFirstFactorStage"; -import VisitPage from "./VisitPage"; -import VerifyUrlContains from "./assertions/VerifyUrlContains"; - -export default async function(driver: WebDriver) { - await VisitPage(driver, "https://login.example.com:8080/#/logout"); - await VerifyUrlContains(driver, "https://login.example.com:8080/#/"); - await VerifyIsFirstFactorStage(driver); -} \ No newline at end of file diff --git a/test/helpers/RegisterTotp.ts b/test/helpers/RegisterTotp.ts deleted file mode 100644 index 7ca1fdd6b..000000000 --- a/test/helpers/RegisterTotp.ts +++ /dev/null @@ -1,13 +0,0 @@ -import SeleniumWebdriver = require("selenium-webdriver"); -import {GetLinkFromFile, GetLinkFromEmail} from './GetIdentityLink'; - -export default async function(driver: SeleniumWebdriver.WebDriver, email?: boolean, timeout: number = 5000){ - await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), timeout) - await driver.findElement(SeleniumWebdriver.By.className("register-totp")).click(); - await driver.sleep(500); - - const link = (email) ? await GetLinkFromEmail() : await GetLinkFromFile(); - await driver.get(link); - await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("base32-secret")), timeout); - return await driver.findElement(SeleniumWebdriver.By.className("base32-secret")).getText(); -}; diff --git a/test/helpers/ValidateTotp.ts b/test/helpers/ValidateTotp.ts deleted file mode 100644 index 4275a45b3..000000000 --- a/test/helpers/ValidateTotp.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Speakeasy from "speakeasy"; -import SeleniumWebdriver, { WebDriver } from 'selenium-webdriver'; - -export default async function(driver: WebDriver, secret: string, timeout: number = 5000) { - const token = Speakeasy.totp({ - secret: secret, - encoding: "base32" - }); - - await driver.wait(SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.id("totp-token")), timeout) - await driver.findElement(SeleniumWebdriver.By.id("totp-token")).sendKeys(token); - - const el = await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id('totp-button')), timeout); - el.click(); -} \ No newline at end of file diff --git a/test/helpers/VisitPage.ts b/test/helpers/VisitPage.ts deleted file mode 100644 index 4b2fadfd0..000000000 --- a/test/helpers/VisitPage.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, url: string) { - await driver.get(url); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyBodyContains.ts b/test/helpers/assertions/VerifyBodyContains.ts deleted file mode 100644 index 19c847195..000000000 --- a/test/helpers/assertions/VerifyBodyContains.ts +++ /dev/null @@ -1,9 +0,0 @@ -import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, match: string, timeout: number = 5000) { - const el = await driver.wait( - SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.tagName('body')), timeout); - - await driver.wait( - SeleniumWebdriver.until.elementTextContains(el, match), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyButtonDoesNotExist.ts b/test/helpers/assertions/VerifyButtonDoesNotExist.ts deleted file mode 100644 index d22c9eb08..000000000 --- a/test/helpers/assertions/VerifyButtonDoesNotExist.ts +++ /dev/null @@ -1,16 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; -import VerifyElementDoesNotExist from "./VerifyElementDoesNotExist"; - -/** - * Verify that an element does not exist. - * - * @param driver The selenium driver - * @param content The content of the button to select. - */ -export default async function(driver: WebDriver, content: string) { - try { - await VerifyElementDoesNotExist(driver, SeleniumWebDriver.By.xpath("//button[text()='" + content + "']")); - } catch (err) { - throw new Error(`Button with content "${content}" should not exist.`); - } -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyButtonHasAppeared.ts b/test/helpers/assertions/VerifyButtonHasAppeared.ts deleted file mode 100644 index 84ce2ad5b..000000000 --- a/test/helpers/assertions/VerifyButtonHasAppeared.ts +++ /dev/null @@ -1,15 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; -import VerifyHasAppeared from "./VerifyHasAppeared"; - -/** - * Verify if a button with given content exists in the DOM. - * @param driver The selenium web driver. - * @param content The content of the button to find in the DOM. - */ -export default async function(driver: WebDriver, content: string) { - try { - await VerifyHasAppeared(driver, SeleniumWebDriver.By.xpath("//button[text()='" + content + "']")); - } catch (err) { - throw new Error(`Button with content "${content}" should have appeared.`); - } -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyElementDoesNotExist.ts b/test/helpers/assertions/VerifyElementDoesNotExist.ts deleted file mode 100644 index 25c6fa972..000000000 --- a/test/helpers/assertions/VerifyElementDoesNotExist.ts +++ /dev/null @@ -1,13 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -/** - * - * @param driver The selenium web driver - * @param locator The locator of the element to check it does not exist. - */ -export default async function(driver: WebDriver, locator: SeleniumWebDriver.Locator) { - const els = await driver.findElements(locator); - if (els.length > 0) { - throw new Error("Element exists."); - } -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyElementExists.ts b/test/helpers/assertions/VerifyElementExists.ts deleted file mode 100644 index aaa69d468..000000000 --- a/test/helpers/assertions/VerifyElementExists.ts +++ /dev/null @@ -1,13 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -/** - * - * @param driver The selenium web driver. - * @param locator The locator of the element to find in the DOM. - */ -export default async function(driver: WebDriver, locator: SeleniumWebDriver.Locator) { - const els = await driver.findElements(locator); - if (els.length == 0) { - throw new Error("Element does not exist."); - } -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyForwardedHeaderDoesNotExist.ts b/test/helpers/assertions/VerifyForwardedHeaderDoesNotExist.ts deleted file mode 100644 index 179e758a5..000000000 --- a/test/helpers/assertions/VerifyForwardedHeaderDoesNotExist.ts +++ /dev/null @@ -1,13 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; -import Util from "util"; - -export default async function(driver: WebDriver, header: string, timeout: number = 5000) { - const el = await driver.wait(SeleniumWebDriver.until.elementLocated( - SeleniumWebDriver.By.tagName("body")), timeout); - const text = await el.getText(); - - const expectedLine = Util.format("\"%s\": ", header); - if (text.indexOf(expectedLine) >= 0) { - throw new Error("Header found."); - } -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyForwardedHeaderIs.ts b/test/helpers/assertions/VerifyForwardedHeaderIs.ts deleted file mode 100644 index 95716d97d..000000000 --- a/test/helpers/assertions/VerifyForwardedHeaderIs.ts +++ /dev/null @@ -1,13 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; -import Util from "util"; - -export default async function(driver: WebDriver, header: string, expectedValue: string, timeout: number = 5000) { - const el = await driver.wait(SeleniumWebDriver.until.elementLocated( - SeleniumWebDriver.By.tagName("body")), timeout); - const text = await el.getText(); - - const expectedLine = Util.format("\"%s\": \"%s\"", header, expectedValue); - if (text.indexOf(expectedLine) < 0) { - throw new Error("Header not found."); - } -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyHasAppeared.ts b/test/helpers/assertions/VerifyHasAppeared.ts deleted file mode 100644 index fea58cc89..000000000 --- a/test/helpers/assertions/VerifyHasAppeared.ts +++ /dev/null @@ -1,5 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, locator: SeleniumWebDriver.Locator, timeout: number = 5000) { - await driver.wait(SeleniumWebDriver.until.elementLocated(locator), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyIsAlreadyAuthenticatedStage.ts b/test/helpers/assertions/VerifyIsAlreadyAuthenticatedStage.ts deleted file mode 100644 index e6e0d07f2..000000000 --- a/test/helpers/assertions/VerifyIsAlreadyAuthenticatedStage.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, timeout: number = 5000) { - await driver.wait(SeleniumWebDriver.until.elementLocated( - SeleniumWebDriver.By.className('already-authenticated-step')), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyIsDuoPushNotificationView.ts b/test/helpers/assertions/VerifyIsDuoPushNotificationView.ts deleted file mode 100644 index 9c327706a..000000000 --- a/test/helpers/assertions/VerifyIsDuoPushNotificationView.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, timeout: number = 5000) { - await driver.wait(SeleniumWebDriver.until.elementLocated( - SeleniumWebDriver.By.className('duo-push-view')), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyIsFirstFactorStage.ts b/test/helpers/assertions/VerifyIsFirstFactorStage.ts deleted file mode 100644 index 335d2be4a..000000000 --- a/test/helpers/assertions/VerifyIsFirstFactorStage.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, timeout: number = 5000) { - await driver.wait(SeleniumWebDriver.until.elementLocated( - SeleniumWebDriver.By.className('first-factor-step')), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyIsOneTimePasswordView.ts b/test/helpers/assertions/VerifyIsOneTimePasswordView.ts deleted file mode 100644 index 650c30447..000000000 --- a/test/helpers/assertions/VerifyIsOneTimePasswordView.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, timeout: number = 5000) { - await driver.wait(SeleniumWebDriver.until.elementLocated( - SeleniumWebDriver.By.className('one-time-password-view')), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyIsSecondFactorStage.ts b/test/helpers/assertions/VerifyIsSecondFactorStage.ts deleted file mode 100644 index 1d7f22a7f..000000000 --- a/test/helpers/assertions/VerifyIsSecondFactorStage.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, timeout: number = 5000) { - await driver.wait(SeleniumWebDriver.until.elementLocated( - SeleniumWebDriver.By.className('second-factor-step')), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyIsSecurityKeyView.ts b/test/helpers/assertions/VerifyIsSecurityKeyView.ts deleted file mode 100644 index c836fc3f0..000000000 --- a/test/helpers/assertions/VerifyIsSecurityKeyView.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, timeout: number = 5000) { - await driver.wait(SeleniumWebDriver.until.elementLocated( - SeleniumWebDriver.By.className('security-key-view')), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyIsUseAnotherMethodView.ts b/test/helpers/assertions/VerifyIsUseAnotherMethodView.ts deleted file mode 100644 index a7a7a058d..000000000 --- a/test/helpers/assertions/VerifyIsUseAnotherMethodView.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, timeout: number = 5000) { - await driver.wait(SeleniumWebDriver.until.elementLocated( - SeleniumWebDriver.By.className('use-another-method-view')), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyNotificationDisplayed.ts b/test/helpers/assertions/VerifyNotificationDisplayed.ts deleted file mode 100644 index 52bda174f..000000000 --- a/test/helpers/assertions/VerifyNotificationDisplayed.ts +++ /dev/null @@ -1,10 +0,0 @@ -import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; -import Assert = require("assert"); - -export default async function(driver: WebDriver, message: string, timeout: number = 5000) { - await driver.wait(SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.className("notification")), timeout) - const notificationEl = driver.findElement(SeleniumWebdriver.By.className("notification")); - const txt = await notificationEl.getText(); - Assert.equal(message, txt); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifySecretObserved.ts b/test/helpers/assertions/VerifySecretObserved.ts deleted file mode 100644 index 1cb1d6ba1..000000000 --- a/test/helpers/assertions/VerifySecretObserved.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { WebDriver } from "selenium-webdriver"; -import VerifyBodyContains from "./VerifyBodyContains"; - -// Verify if the current page contains "This is a very important secret!". -export default async function(driver: WebDriver, timeout: number = 5000) { - await VerifyBodyContains(driver, "This is a very important secret!", timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyUrlContains.ts b/test/helpers/assertions/VerifyUrlContains.ts deleted file mode 100644 index efd92f2e5..000000000 --- a/test/helpers/assertions/VerifyUrlContains.ts +++ /dev/null @@ -1,5 +0,0 @@ -import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, pattern: string, timeout: number = 5000) { - await driver.wait(SeleniumWebdriver.until.urlContains(pattern), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/VerifyUrlIs.ts b/test/helpers/assertions/VerifyUrlIs.ts deleted file mode 100644 index 136a5719d..000000000 --- a/test/helpers/assertions/VerifyUrlIs.ts +++ /dev/null @@ -1,5 +0,0 @@ -import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, url: string, timeout: number = 5000) { - await driver.wait(SeleniumWebdriver.until.urlIs(url), timeout); -} \ No newline at end of file diff --git a/test/helpers/assertions/WaitUrlIs.ts b/test/helpers/assertions/WaitUrlIs.ts deleted file mode 100644 index a6a8477b8..000000000 --- a/test/helpers/assertions/WaitUrlIs.ts +++ /dev/null @@ -1,3 +0,0 @@ -import VerifyUrlIs from "./VerifyUrlIs"; - -export default VerifyUrlIs; \ No newline at end of file diff --git a/test/helpers/behaviors/ClearFieldById.ts b/test/helpers/behaviors/ClearFieldById.ts deleted file mode 100644 index dd45efed1..000000000 --- a/test/helpers/behaviors/ClearFieldById.ts +++ /dev/null @@ -1,9 +0,0 @@ -import SeleniumWebdriver, { WebDriver, Key, ActionSequence } from "selenium-webdriver"; - -export default async function(driver: WebDriver, fieldId: string, timeout: number = 5000) { - const element = await driver.wait( - SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.id(fieldId)), timeout) - - await element.sendKeys(Key.chord(Key.CONTROL, "a", Key.BACK_SPACE)); -}; \ No newline at end of file diff --git a/test/helpers/behaviors/ClickOnButton.ts b/test/helpers/behaviors/ClickOnButton.ts deleted file mode 100644 index abcf5a367..000000000 --- a/test/helpers/behaviors/ClickOnButton.ts +++ /dev/null @@ -1,8 +0,0 @@ -import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; - -export default async function(driver: WebDriver, text: string, timeout: number = 5000) { - const element = await driver.wait( - SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.xpath("//button[text()='" + text + "']")), timeout) - await element.click(); -}; \ No newline at end of file diff --git a/test/helpers/behaviors/LoginOneFactor.ts b/test/helpers/behaviors/LoginOneFactor.ts deleted file mode 100644 index 06cbb77eb..000000000 --- a/test/helpers/behaviors/LoginOneFactor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { WebDriver } from "selenium-webdriver"; -import FillLoginPageAndClick from "../FillLoginPageAndClick"; -import VerifyUrlIs from "../assertions/VerifyUrlIs"; -import VisitPageAndWaitUrlIs from "./VisitPageAndWaitUrlIs"; - -export default async function( - driver: WebDriver, - username: string, - password: string, - targetUrl: string, - timeout: number = 5000) { - - await VisitPageAndWaitUrlIs(driver, `https://login.example.com:8080/#/?rd=${targetUrl}`, timeout); - await FillLoginPageAndClick(driver, username, password, false, timeout); - await VerifyUrlIs(driver, targetUrl, timeout); -}; \ No newline at end of file diff --git a/test/helpers/behaviors/RegisterAndLoginTwoFactor.ts b/test/helpers/behaviors/RegisterAndLoginTwoFactor.ts deleted file mode 100644 index f477e0d3a..000000000 --- a/test/helpers/behaviors/RegisterAndLoginTwoFactor.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { WebDriver } from "selenium-webdriver"; -import LoginAndRegisterTotp from "../LoginAndRegisterTotp"; -import VerifyUrlIs from "../assertions/VerifyUrlIs"; -import VisitPageAndWaitUrlIs from "./VisitPageAndWaitUrlIs"; -import ValidateTotp from "../ValidateTotp"; - -export default async function( - driver: WebDriver, - username: string, - password: string, - email: boolean = false, - targetUrl: string = "https://login.example.com:8080/#/", - timeout: number = 5000) { - - const secret = await LoginAndRegisterTotp(driver, username, password, email, timeout); - await VisitPageAndWaitUrlIs(driver, `https://login.example.com:8080/#/?rd=${targetUrl}`, timeout); - await ValidateTotp(driver, secret, timeout); - await VerifyUrlIs(driver, targetUrl, timeout); - return secret; -}; \ No newline at end of file diff --git a/test/helpers/behaviors/ResetPassword.ts b/test/helpers/behaviors/ResetPassword.ts deleted file mode 100644 index 1111680f5..000000000 --- a/test/helpers/behaviors/ResetPassword.ts +++ /dev/null @@ -1,23 +0,0 @@ -import SeleniumWebDriver from "selenium-webdriver" -import VisitPageAndWaitUrlIs from "./VisitPageAndWaitUrlIs"; -import ClickOnLink from "../ClickOnLink"; -import VerifyUrlIs from "../assertions/VerifyUrlIs"; -import FillField from "../FillField"; -import ClickOn from "../ClickOn"; -import { GetLinkFromEmail } from "../GetIdentityLink"; - -export default async function(driver: SeleniumWebDriver.WebDriver, username: string, password: string, timeout: number = 5000) { - await VisitPageAndWaitUrlIs(driver, "https://login.example.com:8080/#/"); - await ClickOnLink(driver, "Forgot password\?"); - await VerifyUrlIs(driver, "https://login.example.com:8080/#/forgot-password"); - await FillField(driver, "username", username); - await ClickOn(driver, SeleniumWebDriver.By.id('next-button')); - await VerifyUrlIs(driver, 'https://login.example.com:8080/#/confirmation-sent'); - - await driver.sleep(500); // Simulate the time it takes to receive the e-mail. - const link = await GetLinkFromEmail(); - await VisitPageAndWaitUrlIs(driver, link); - await FillField(driver, "password1", password); - await FillField(driver, "password2", password); - await ClickOn(driver, SeleniumWebDriver.By.id('reset-button')); -} \ No newline at end of file diff --git a/test/helpers/behaviors/VisitPageAndWaitUrlIs.ts b/test/helpers/behaviors/VisitPageAndWaitUrlIs.ts deleted file mode 100644 index f78b45721..000000000 --- a/test/helpers/behaviors/VisitPageAndWaitUrlIs.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { WebDriver } from "selenium-webdriver"; -import VerifyUrlIs from "../assertions/VerifyUrlIs"; -import VisitPage from "../VisitPage"; - -export default async function(driver: WebDriver, url: string, timeout: number = 5000) { - await VisitPage(driver, url); - await VerifyUrlIs(driver, url, timeout); -} \ No newline at end of file diff --git a/test/helpers/context/AutheliaSuite.ts b/test/helpers/context/AutheliaSuite.ts deleted file mode 100644 index c00cfa1c6..000000000 --- a/test/helpers/context/AutheliaSuite.ts +++ /dev/null @@ -1,26 +0,0 @@ -interface AutheliaSuiteType { - (suitePath: string, cb: (this: Mocha.ISuiteCallbackContext) => void): Mocha.ISuite; - only: (suitePath: string, cb: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite; -} - -function AutheliaSuiteBase(suitePath: string, - cb: (this: Mocha.ISuiteCallbackContext) => void, - context: (description: string, ctx: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite) { - const suite = suitePath.split('/').slice(-1)[0]; - return context('Suite: ' + suite, function(this: Mocha.ISuiteCallbackContext) { - cb.call(this); - }); -} - -const AutheliaSuite = function(suitePath: string, - cb: (this: Mocha.ISuiteCallbackContext) => void) { - return AutheliaSuiteBase(suitePath, cb, describe); -} - - -AutheliaSuite.only = function(suitePath: string, - cb: (this: Mocha.ISuiteCallbackContext) => void) { - return AutheliaSuiteBase(suitePath, cb, describe.only); -} - -export default AutheliaSuite as AutheliaSuiteType; \ No newline at end of file diff --git a/test/helpers/context/DockerCompose.ts b/test/helpers/context/DockerCompose.ts deleted file mode 100644 index 0c9bb90f1..000000000 --- a/test/helpers/context/DockerCompose.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { exec } from '../../helpers/utils/exec'; -import { execSync } from 'child_process'; - -class DockerCompose { - private commandPrefix: string; - - constructor(composeFiles: string[]) { - this.commandPrefix = 'docker-compose ' + composeFiles.map((f) => '-f ' + f).join(' '); - } - - async up() { - return await exec(this.commandPrefix + ' up -d'); - } - - async down() { - return await exec(this.commandPrefix + ' down'); - } - - async restart(service: string) { - return await exec(this.commandPrefix + ' restart ' + service); - } - - async ps() { - return Promise.resolve(execSync(this.commandPrefix + ' ps').toString('utf-8')); - } - - async logs(service: string) { - await exec(this.commandPrefix + ' logs ' + service) - } -} - -export default DockerCompose; \ No newline at end of file diff --git a/test/helpers/context/DockerEnvironment.ts b/test/helpers/context/DockerEnvironment.ts deleted file mode 100644 index fe955ce24..000000000 --- a/test/helpers/context/DockerEnvironment.ts +++ /dev/null @@ -1,23 +0,0 @@ -import DockerCompose from "./DockerCompose"; - -class DockerEnvironment { - private dockerCompose: DockerCompose; - - constructor(composeFiles: string[]) { - this.dockerCompose = new DockerCompose(composeFiles); - } - - async start() { - await this.dockerCompose.up(); - } - - async logs(service: string) { - await this.dockerCompose.logs(service); - } - - async stop() { - await this.dockerCompose.down(); - } -} - -export default DockerEnvironment; \ No newline at end of file diff --git a/test/helpers/context/WithDriver.ts b/test/helpers/context/WithDriver.ts deleted file mode 100644 index b091d2405..000000000 --- a/test/helpers/context/WithDriver.ts +++ /dev/null @@ -1,43 +0,0 @@ -require("chromedriver"); -import chrome from 'selenium-webdriver/chrome'; -import SeleniumWebdriver, { WebDriver, ProxyConfig } from "selenium-webdriver"; - -export async function StartDriver(proxy?: ProxyConfig) { - let options = new chrome.Options(); - - if (process.env['HEADLESS'] == 'y') { - options = options.headless(); - } - - let driverBuilder = new SeleniumWebdriver.Builder() - .forBrowser("chrome"); - - if (proxy) { - options = options.addArguments(`--proxy-server=${proxy.httpProxy}`) - } - - driverBuilder = driverBuilder.setChromeOptions(options); - return await driverBuilder.build(); -} - -export async function StopDriver(driver: WebDriver) { - return await driver.quit(); -} - -export default function(forEach: boolean = false) { - if (forEach) { - beforeEach(async function() { - this.driver = await StartDriver(); - }); - afterEach(async function() { - await StopDriver(this.driver); - }); - } else { - before(async function() { - this.driver = await StartDriver(); - }); - after(async function() { - await StopDriver(this.driver) - }); - } -} \ No newline at end of file diff --git a/test/helpers/scenarii/AuthenticationBlacklisting.ts b/test/helpers/scenarii/AuthenticationBlacklisting.ts deleted file mode 100644 index 770e86f12..000000000 --- a/test/helpers/scenarii/AuthenticationBlacklisting.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { StartDriver, StopDriver } from "../../helpers/context/WithDriver"; -import LoginAs from "../../helpers/LoginAs"; -import VerifyNotificationDisplayed from "../../helpers/assertions/VerifyNotificationDisplayed"; -import VerifyIsSecondFactorStage from "../../helpers/assertions/VerifyIsSecondFactorStage"; -import ClearFieldById from "../behaviors/ClearFieldById"; - -export default function(regulationMilliseconds: number) { - return function () { - describe('Authelia regulates authentications when a hacker is brute forcing', function() { - this.timeout(30000); - beforeEach(async function() { - this.driver = await StartDriver(); - }); - - afterEach(async function() { - await StopDriver(this.driver); - }); - - it("should return an error message when providing correct credentials the 4th time.", async function() { - await LoginAs(this.driver, "james", "bad-password"); - await VerifyNotificationDisplayed(this.driver, "Authentication failed. Check your credentials."); - await ClearFieldById(this.driver, "username"); - - await LoginAs(this.driver, "james", "bad-password"); - await VerifyNotificationDisplayed(this.driver, "Authentication failed. Check your credentials."); - await ClearFieldById(this.driver, "username"); - - await LoginAs(this.driver, "james", "bad-password"); - await VerifyNotificationDisplayed(this.driver, "Authentication failed. Check your credentials."); - await ClearFieldById(this.driver, "username"); - - // when providing good credentials, the hacker is regulated and see same message as previously. - await LoginAs(this.driver, "james", "bad-password"); - await VerifyNotificationDisplayed(this.driver, "Please retry in a few minutes."); - await ClearFieldById(this.driver, "username"); - - // Wait the regulation ban time before retrying with correct credentials. - // It should authenticate normally. - await this.driver.sleep(regulationMilliseconds); - await LoginAs(this.driver, "james", "password"); - await VerifyIsSecondFactorStage(this.driver); - }); - }); - } -} \ No newline at end of file diff --git a/test/helpers/scenarii/SingleFactorAuthentication.ts b/test/helpers/scenarii/SingleFactorAuthentication.ts deleted file mode 100644 index c1a74c3a2..000000000 --- a/test/helpers/scenarii/SingleFactorAuthentication.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Logout from "../Logout"; -import { StartDriver, StopDriver } from "../context/WithDriver"; -import LoginOneFactor from "../behaviors/LoginOneFactor"; -import VerifySecretObserved from "../assertions/VerifySecretObserved"; -import VisitPage from "../VisitPage"; -import VerifyIsSecondFactorStage from "../assertions/VerifyIsSecondFactorStage"; -import VerifyUrlContains from "../assertions/VerifyUrlContains"; - -/* - * Those tests are related to single factor protected resources. - */ -export default function(timeout: number = 5000) { - return function() { - beforeEach(async function() { - this.driver = await StartDriver(); - }); - - afterEach(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - }); - - it("should redirect user after first stage", async function() { - await LoginOneFactor(this.driver, "john", "password", "https://singlefactor.example.com:8080/secret.html", timeout); - await VerifySecretObserved(this.driver, timeout); - }); - - it("should redirect to portal if not enough authorized", async function() { - await LoginOneFactor(this.driver, "john", "password", "https://singlefactor.example.com:8080/secret.html", timeout); - await VisitPage(this.driver, "https://admin.example.com:8080/secret.html"); - - // And the user should end up on the second factor page. - await VerifyIsSecondFactorStage(this.driver, timeout); - }); - } -} \ No newline at end of file diff --git a/test/helpers/scenarii/TwoFactorAuthentication.ts b/test/helpers/scenarii/TwoFactorAuthentication.ts deleted file mode 100644 index 96e2d28bf..000000000 --- a/test/helpers/scenarii/TwoFactorAuthentication.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { StartDriver, StopDriver } from "../context/WithDriver"; -import RegisterAndLoginTwoFactor from "../behaviors/RegisterAndLoginTwoFactor"; -import VerifyUrlIs from "../assertions/VerifyUrlIs"; -import Logout from "../Logout"; - -export default function (timeout: number = 5000) { - return function() { - describe('The user is redirected to target url upon successful authentication', function() { - before(async function() { - this.driver = await StartDriver(); - await RegisterAndLoginTwoFactor( - this.driver, - 'john', "password", true, - 'https://admin.example.com:8080/secret.html', - timeout); - }); - - after(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - }); - - it('should redirect the user', async function() { - await VerifyUrlIs( - this.driver, - 'https://admin.example.com:8080/secret.html', - timeout); - }); - }); - } -} \ No newline at end of file diff --git a/test/helpers/utils/Requests.ts b/test/helpers/utils/Requests.ts deleted file mode 100644 index 1605a4dd1..000000000 --- a/test/helpers/utils/Requests.ts +++ /dev/null @@ -1,65 +0,0 @@ -import Request from 'request-promise'; -import Fetch from 'node-fetch'; -import Assert from 'assert'; -import { StatusCodeError } from 'request-promise/errors'; - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - -export async function GET_ExpectError(url: string, headers: {[key: string]: string}, statusCode: number) { - try { - await Request.get(url, { - json: true, - rejectUnauthorized: false, - headers: headers, - }); - throw new Error('No response'); - } catch (e) { - if (e instanceof StatusCodeError) { - Assert.equal(e.statusCode, statusCode); - return; - } - } - return; -} - -// Sent a GET request to the url and expect a 401 -export async function GET_Expect401(url: string, headers: {[key: string]: string} = {}) { - return await GET_ExpectError(url, headers, 401); -} - -export async function GET_Expect403(url: string, headers: {[key: string]: string} = {}) { - return await GET_ExpectError(url, headers, 403); -} - -export async function GET_Expect502(url: string, headers: {[key: string]: string} = {}) { - return await GET_ExpectError(url, headers, 502); -} - -export async function POST_Expect403(url: string, body?: any) { - try { - await Request.post(url, { - json: true, - rejectUnauthorized: false, - body - }); - throw new Error('No response'); - } catch (e) { - if (e instanceof StatusCodeError) { - Assert.equal(e.statusCode, 403); - return; - } - } - return; -} - -export async function GET_ExpectRedirect(url: string, redirectionUrl: string, headers: {[key: string]: string} = {}) { - const response = await Fetch(url, {redirect: 'manual', headers: headers}); - - if (response.status == 302) { - const body = await response.text(); - Assert.equal(body, 'Found. Redirecting to ' + redirectionUrl); - return; - } - - throw new Error('No redirect'); -} \ No newline at end of file diff --git a/test/helpers/utils/WaitUntil.ts b/test/helpers/utils/WaitUntil.ts deleted file mode 100644 index 1c4c81262..000000000 --- a/test/helpers/utils/WaitUntil.ts +++ /dev/null @@ -1,30 +0,0 @@ -import sleep from "./sleep"; - -export default function WaitUntil( - fn: () => Promise, timeout: number = 15000, - interval: number = 1000, waitBefore: number = 0, waitAfter: number = 0): Promise { - - return new Promise(async (resolve, reject) => { - const timer = setTimeout(() => { throw new Error('Timeout') }, timeout); - if (waitBefore > 0) - await sleep(waitBefore); - while (true) { - try { - const res = await fn(); - if (res && res === true) { - clearTimeout(timer); - break; - } - await sleep(interval); - } catch (err) { - console.error(err); - reject(err); - return; - } - } - - if (waitAfter > 0) - await sleep(waitAfter); - resolve(); - }); -} \ No newline at end of file diff --git a/test/helpers/utils/exec.ts b/test/helpers/utils/exec.ts deleted file mode 100644 index 29be7ec80..000000000 --- a/test/helpers/utils/exec.ts +++ /dev/null @@ -1,41 +0,0 @@ -var spawn = require('child_process').spawn; - -interface Options { - cwd?: string; - env?: {[key: string]: string}; - debug?: boolean; -} - -export function exec(command: string, options?: Options): Promise { - return new Promise((resolve, reject) => { - const spawnOptions = { - shell: true, - } as any; - - if (options && options.cwd) { - spawnOptions['cwd'] = options.cwd; - } - - if (options && options.env) { - spawnOptions['env'] = { - ...options.env, - ...process.env, - } - } - - if (options && options.debug) { - console.log('>>> ' + command); - } - const cmd = spawn(command, spawnOptions); - - cmd.stdout.pipe(process.stdout); - cmd.stderr.pipe(process.stderr); - cmd.on('exit', (statusCode: number) => { - if (statusCode == 0) { - resolve(); - return; - } - reject(new Error('\'' + command + '\' exited with status code ' + statusCode)); - }); - }); -} diff --git a/test/helpers/utils/execPromise.ts b/test/helpers/utils/execPromise.ts deleted file mode 100644 index e3d5aabd9..000000000 --- a/test/helpers/utils/execPromise.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { exec } from 'child_process'; - -function execPromise(command: string) { - return new Promise(function(resolve, reject) { - exec(command, (error, stdout, stderr) => { - if (error) { - reject(error); - return; - } - - resolve(stdout.trim()); - }); - }); -} - -export default execPromise; \ No newline at end of file diff --git a/test/helpers/utils/sleep.ts b/test/helpers/utils/sleep.ts deleted file mode 100644 index d8321ed06..000000000 --- a/test/helpers/utils/sleep.ts +++ /dev/null @@ -1,5 +0,0 @@ - - -export default function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/test/suites/.gitignore b/test/suites/.gitignore deleted file mode 100644 index 83d1ce3ce..000000000 --- a/test/suites/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -users_database.test.yml - -private-*/ \ No newline at end of file diff --git a/test/suites/BypassAll/README.md b/test/suites/BypassAll/README.md deleted file mode 100644 index 22e53301d..000000000 --- a/test/suites/BypassAll/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# ACL full bypass suite - -This suite has been created to test Authelia with a bypass policy on all resources - -## Components - -Authelia, nginx, fake webmail for registering devices. - -## Tests - -Check access to secret of multiple domains. \ No newline at end of file diff --git a/test/suites/BypassAll/scenarii/BypassPolicy.ts b/test/suites/BypassAll/scenarii/BypassPolicy.ts deleted file mode 100644 index c556ffd02..000000000 --- a/test/suites/BypassAll/scenarii/BypassPolicy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; -import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; - -export default function() { - before(async function() { - this.driver = await StartDriver(); - }); - - after(async function () { - await StopDriver(this.driver); - }); - - it('should have access to admin.example.com/secret.html', async function () { - await VisitPageAndWaitUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - await VerifySecretObserved(this.driver); - }); - - it('should have access to public.example.com/secret.html', async function () { - await VisitPageAndWaitUrlIs(this.driver, "https://public.example.com:8080/secret.html"); - await VerifySecretObserved(this.driver); - }); -} \ No newline at end of file diff --git a/test/suites/BypassAll/scenarii/CustomHeadersForwarded.ts b/test/suites/BypassAll/scenarii/CustomHeadersForwarded.ts deleted file mode 100644 index 1d2b923ea..000000000 --- a/test/suites/BypassAll/scenarii/CustomHeadersForwarded.ts +++ /dev/null @@ -1,52 +0,0 @@ -import Logout from "../../../helpers/Logout"; -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import VerifyForwardedHeaderIs from "../../../helpers/assertions/VerifyForwardedHeaderIs"; -import LoginOneFactor from "../../../helpers/behaviors/LoginOneFactor"; -import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; -import VerifyButtonDoesNotExist from "../../../helpers/assertions/VerifyButtonDoesNotExist"; - -export default function() { - describe("Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded when available", function() { - this.timeout(100000); - - describe("Headers are not forwarded for anonymous user", function() { - before(async function() { - this.driver = await StartDriver(); - await VisitPageAndWaitUrlIs(this.driver, "https://public.example.com:8080/headers"); - }); - - after(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - }); - - it("should check header 'Custom-Forwarded-User' does not exist", async function() { - await VerifyButtonDoesNotExist(this.driver, 'Custom-Forwarded-User'); - }); - - it("should check header 'Custom-Forwarded-Groups' does not exist", async function() { - await VerifyButtonDoesNotExist(this.driver, 'Custom-Forwarded-Groups'); - }); - }); - - describe("Headers are forwarded for authenticated user", function() { - before(async function() { - this.driver = await StartDriver(); - await LoginOneFactor(this.driver, "john", "password", "https://public.example.com:8080/headers"); - }); - - after(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - }); - - it("should see header 'Custom-Forwarded-User' set to 'john'", async function() { - await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-User', 'john'); - }); - - it("should see header 'Custom-Forwarded-Groups' set to 'admins,dev'", async function() { - await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-Groups', 'admins,dev'); - }); - }); - }); -} \ No newline at end of file diff --git a/test/suites/BypassAll/scenarii/NoDefaultRedirectionUrl.ts b/test/suites/BypassAll/scenarii/NoDefaultRedirectionUrl.ts deleted file mode 100644 index 3af66283c..000000000 --- a/test/suites/BypassAll/scenarii/NoDefaultRedirectionUrl.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import LoginAs from "../../../helpers/LoginAs"; -import VerifyIsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage"; -import ClickOnLink from "../../../helpers/ClickOnLink"; -import VerifyIsUseAnotherMethodView from "../../../helpers/assertions/VerifyIsUseAnotherMethodView"; -import ClickOnButton from "../../../helpers/behaviors/ClickOnButton"; -import Request from 'request-promise'; -import VerifyIsAlreadyAuthenticatedStage from "../../../helpers/assertions/VerifyIsAlreadyAuthenticatedStage"; - -export default function() { - before(async function() { - this.driver = await StartDriver(); - - // Configure the fake API to return allowing response. - await Request('https://duo.example.com/allow', {method: 'POST'}); - }); - - after(async function () { - await StopDriver(this.driver); - }); - - it('should send user to already authenticated page', async function() { - await LoginAs(this.driver, "john", "password"); - await VerifyIsSecondFactorStage(this.driver); - - await ClickOnLink(this.driver, 'Use another method'); - await VerifyIsUseAnotherMethodView(this.driver); - await ClickOnButton(this.driver, 'Duo Push Notification'); - await VerifyIsAlreadyAuthenticatedStage(this.driver, 10000); - - await ClickOnButton(this.driver, "Logout"); - }); -} \ No newline at end of file diff --git a/test/suites/BypassAll/test.ts b/test/suites/BypassAll/test.ts deleted file mode 100644 index 0a8b6e444..000000000 --- a/test/suites/BypassAll/test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import AutheliaSuite from "../../helpers/context/AutheliaSuite"; -import { exec } from '../../helpers/utils/exec'; -import BypassPolicy from "./scenarii/BypassPolicy"; -import NoDefaultRedirectionUrl from "./scenarii/NoDefaultRedirectionUrl"; -import CustomHeadersForwarded from "./scenarii/CustomHeadersForwarded"; - -process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any; - -process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any; - -AutheliaSuite(__dirname, function () { - this.timeout(10000); - - beforeEach(async function () { - await exec(`cp ${__dirname}/../../../internal/suites/BypassAll/users.yml /tmp/authelia/suites/BypassAll/users.yml`); - }); - - describe('Bypass policy', BypassPolicy); - describe("No default redirection", NoDefaultRedirectionUrl); - describe("Custom headers forwarded on bypass", CustomHeadersForwarded); -}); \ No newline at end of file diff --git a/test/suites/DuoPush/README.md b/test/suites/DuoPush/README.md deleted file mode 100644 index d8d83dd3d..000000000 --- a/test/suites/DuoPush/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Duo Push Notification suite - -This suite has been created to test Authelia against the Duo API for push notifications. -It allows a user to validate second factor with a mobile phone. - -## Components - -Authelia, nginx, Duo fake API - -## Tests - -Test allowed and denied access via push notifications. \ No newline at end of file diff --git a/test/suites/DuoPush/scenarii/DuoPushNotification.ts b/test/suites/DuoPush/scenarii/DuoPushNotification.ts deleted file mode 100644 index ab8875d29..000000000 --- a/test/suites/DuoPush/scenarii/DuoPushNotification.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import LoginAs from "../../../helpers/LoginAs"; -import VerifyIsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage"; -import ClickOnLink from "../../../helpers/ClickOnLink"; -import VerifyIsUseAnotherMethodView from "../../../helpers/assertions/VerifyIsUseAnotherMethodView"; -import ClickOnButton from "../../../helpers/behaviors/ClickOnButton"; -import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; -import Request from 'request-promise'; -import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs"; -import VerifyHasAppeared from "../../../helpers/assertions/VerifyHasAppeared"; -import SeleniumWebDriver from "selenium-webdriver"; -import VisitPage from "../../../helpers/VisitPage"; - - -export default function() { - before(async function() { - this.driver = await StartDriver(); - }); - - after(async function () { - await StopDriver(this.driver); - }); - - describe('Allow access', function() { - before(async function() { - // Configure the fake API to return allowing response. - await Request('https://duo.example.com/allow', {method: 'POST'}); - }); - - it('should grant access with Duo API', async function() { - await LoginAs(this.driver, "john", "password", "https://secure.example.com:8080/secret.html"); - await VerifyIsSecondFactorStage(this.driver); - - await ClickOnLink(this.driver, 'Use another method'); - await VerifyIsUseAnotherMethodView(this.driver); - await ClickOnButton(this.driver, 'Duo Push Notification'); - - await VerifyUrlIs(this.driver, "https://secure.example.com:8080/secret.html"); - await VerifySecretObserved(this.driver); - - await VisitPage(this.driver, "https://login.example.com:8080/#/"); - await ClickOnButton(this.driver, "Logout"); - }); - }); - - describe('Deny access', function() { - before(async function() { - // Configure the fake API to return denying response. - await Request('https://duo.example.com/deny', {method: 'POST'}); - }); - - it('should grant access with Duo API', async function() { - await LoginAs(this.driver, "john", "password", "https://secure.example.com:8080/secret.html"); - await VerifyIsSecondFactorStage(this.driver); - - await ClickOnLink(this.driver, 'Use another method'); - await VerifyIsUseAnotherMethodView(this.driver); - await ClickOnButton(this.driver, 'Duo Push Notification'); - - // The retry button appeared. - await VerifyHasAppeared(this.driver, SeleniumWebDriver.By.tagName("button")); - }); - }); -} \ No newline at end of file diff --git a/test/suites/DuoPush/scenarii/Prefered2faMethod.ts b/test/suites/DuoPush/scenarii/Prefered2faMethod.ts deleted file mode 100644 index ea776fcbd..000000000 --- a/test/suites/DuoPush/scenarii/Prefered2faMethod.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import LoginAs from "../../../helpers/LoginAs"; -import VerifyIsOneTimePasswordView from "../../../helpers/assertions/VerifyIsOneTimePasswordView"; -import ClickOnLink from "../../../helpers/ClickOnLink"; -import VerifyIsUseAnotherMethodView from "../../../helpers/assertions/VerifyIsUseAnotherMethodView"; -import ClickOnButton from "../../../helpers/behaviors/ClickOnButton"; -import VerifyIsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage"; -import VerifyIsDuoPushNotificationView from "../../../helpers/assertions/VerifyIsDuoPushNotificationView"; - - -// This fixture tests that the latest used method is still used when the user gets back. -export default function() { - before(async function() { - this.driver = await StartDriver(); - }); - - after(async function() { - await StopDriver(this.driver); - }); - - // The default method is TOTP and then everytime the user switches method, - // it get remembered and reloaded during next authentication. - it('should serve the correct method', async function() { - await LoginAs(this.driver, "john", "password", "https://secure.example.com:8080/"); - await VerifyIsSecondFactorStage(this.driver); - - await ClickOnLink(this.driver, 'Use another method'); - await VerifyIsUseAnotherMethodView(this.driver); - await ClickOnButton(this.driver, 'Duo Push Notification'); - - // Verify that the user is redirected to the new method - await VerifyIsDuoPushNotificationView(this.driver); - await ClickOnLink(this.driver, "Logout"); - - // Login with another user to check that he gets TOTP view. - await LoginAs(this.driver, "harry", "password", "https://secure.example.com:8080/"); - await VerifyIsOneTimePasswordView(this.driver); - await ClickOnLink(this.driver, "Logout"); - - // Log john again to check that the prefered method has been persisted - await LoginAs(this.driver, "john", "password", "https://secure.example.com:8080/"); - await VerifyIsDuoPushNotificationView(this.driver); - - // Restore the prefered method to one-time password. - await ClickOnLink(this.driver, 'Use another method'); - await VerifyIsUseAnotherMethodView(this.driver); - await ClickOnButton(this.driver, 'One-Time Password'); - await VerifyIsOneTimePasswordView(this.driver); - await ClickOnLink(this.driver, "Logout"); - }); -} \ No newline at end of file diff --git a/test/suites/DuoPush/test.ts b/test/suites/DuoPush/test.ts deleted file mode 100644 index f7dad04b4..000000000 --- a/test/suites/DuoPush/test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import AutheliaSuite from "../../helpers/context/AutheliaSuite"; -import { exec } from '../../helpers/utils/exec'; -import DuoPushNotification from "./scenarii/DuoPushNotification"; -import Prefered2faMethod from "./scenarii/Prefered2faMethod"; - -// required to query duo-api over https -process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any; - -AutheliaSuite(__dirname, function () { - this.timeout(10000); - - beforeEach(async function () { - await exec(`cp ${__dirname}/../../../internal/suites/DuoPush/users.yml /tmp/authelia/suites/DuoPush/users.yml`); - }); - - describe("Duo Push Notication", DuoPushNotification); - describe("Prefered 2FA methods", Prefered2faMethod); -}); \ No newline at end of file diff --git a/test/suites/HighAvailability/README.md b/test/suites/HighAvailability/README.md deleted file mode 100644 index 0cd3d722d..000000000 --- a/test/suites/HighAvailability/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# High-availability suite - -This suite is made to test Authelia in a *complete* environment, that is, with -all components making Authelia highly available. - -## Components - -This suite will spawn nginx as the edge reverse proxy, redis and mariadb for storing -user sessions and configurations, LDAP for storing user accounts and authenticating, -as well as a few helpers such as a fake webmail to receive e-mails sent by Authelia -and httpbin to check headers forwarded by Authelia. - -## Tests - -There is broad range of tests in this suite. Check out in the *scenarii* directory. \ No newline at end of file diff --git a/test/suites/HighAvailability/environment.ts b/test/suites/HighAvailability/environment.ts deleted file mode 100644 index 28cb8b05b..000000000 --- a/test/suites/HighAvailability/environment.ts +++ /dev/null @@ -1,17 +0,0 @@ -const composeFiles = [ - 'docker-compose.yml', - 'example/compose/authelia/docker-compose.backend.yml', - 'example/compose/authelia/docker-compose.frontend.yml', - 'example/compose/mariadb/docker-compose.yml', - 'example/compose/redis/docker-compose.yml', - 'example/compose/nginx/backend/docker-compose.yml', - 'example/compose/nginx/portal/docker-compose.yml', - 'example/compose/smtp/docker-compose.yml', - 'example/compose/httpbin/docker-compose.yml', - 'example/compose/ldap/docker-compose.admin.yml', // This is just used for administration, not for testing. - 'example/compose/ldap/docker-compose.yml' -] - -export { - composeFiles, -}; \ No newline at end of file diff --git a/test/suites/HighAvailability/scenarii/AccessControl.ts b/test/suites/HighAvailability/scenarii/AccessControl.ts deleted file mode 100644 index 37bb61816..000000000 --- a/test/suites/HighAvailability/scenarii/AccessControl.ts +++ /dev/null @@ -1,109 +0,0 @@ -import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; -import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import ValidateTotp from "../../../helpers/ValidateTotp"; -import Logout from "../../../helpers/Logout"; -import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; -import VerifyBodyContains from "../../../helpers/assertions/VerifyBodyContains"; -import VerifyUrlIs from "../../../helpers/assertions/WaitUrlIs"; - -async function ShouldHaveAccessTo(url: string) { - it('should have access to ' + url, async function() { - await VisitPageAndWaitUrlIs(this.driver, url); - await VerifySecretObserved(this.driver); - }) -} - -async function ShouldNotHaveAccessTo(url: string) { - it('should not have access to ' + url, async function() { - await VisitPageAndWaitUrlIs(this.driver, url); - await VerifyBodyContains(this.driver, "403 Forbidden"); - }) -} - -// we verify that the user has only access to want he is granted to. -export default function() { - // We ensure that bob has access to what he is granted to - describe('Permissions of user john', function() { - before(async function() { - this.driver = await StartDriver(); - const secret = await LoginAndRegisterTotp(this.driver, "john", "password", true); - await VisitPageAndWaitUrlIs(this.driver, 'https://login.example.com:8080/#/'); - await ValidateTotp(this.driver, secret); - await VerifyUrlIs(this.driver, "https://home.example.com:8080/"); - }); - - after(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - }); - - ShouldHaveAccessTo('https://public.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://secure.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://dev.example.com:8080/groups/admin/secret.html'); - ShouldHaveAccessTo('https://dev.example.com:8080/groups/dev/secret.html'); - ShouldHaveAccessTo('https://dev.example.com:8080/users/john/secret.html'); - ShouldHaveAccessTo('https://dev.example.com:8080/users/harry/secret.html'); - ShouldHaveAccessTo('https://dev.example.com:8080/users/bob/secret.html'); - ShouldHaveAccessTo('https://admin.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://mx1.mail.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://singlefactor.example.com:8080/secret.html'); - ShouldNotHaveAccessTo('https://mx2.mail.example.com:8080/secret.html'); - }) - - // We ensure that bob has access to what he is granted to - describe('Permissions of user bob', function() { - before(async function() { - this.driver = await StartDriver(); - const secret = await LoginAndRegisterTotp(this.driver, "bob", "password", true); - await VisitPageAndWaitUrlIs(this.driver, 'https://login.example.com:8080/#/'); - await ValidateTotp(this.driver, secret); - await VerifyUrlIs(this.driver, "https://home.example.com:8080/"); - }); - - after(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - }); - - ShouldHaveAccessTo('https://public.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://secure.example.com:8080/secret.html'); - ShouldNotHaveAccessTo('https://dev.example.com:8080/groups/admin/secret.html'); - ShouldHaveAccessTo('https://dev.example.com:8080/groups/dev/secret.html'); - ShouldNotHaveAccessTo('https://dev.example.com:8080/users/john/secret.html'); - ShouldNotHaveAccessTo('https://dev.example.com:8080/users/harry/secret.html'); - ShouldHaveAccessTo('https://dev.example.com:8080/users/bob/secret.html'); - ShouldNotHaveAccessTo('https://admin.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://mx1.mail.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://singlefactor.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://mx2.mail.example.com:8080/secret.html'); - }); - - // We ensure that harry has access to what he is granted to - describe('Permissions of user harry', function() { - before(async function() { - this.driver = await StartDriver(); - const secret = await LoginAndRegisterTotp(this.driver, "harry", "password", true); - await VisitPageAndWaitUrlIs(this.driver, 'https://login.example.com:8080/#/'); - await ValidateTotp(this.driver, secret); - await VerifyUrlIs(this.driver, "https://home.example.com:8080/"); - }); - - after(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - }); - - ShouldHaveAccessTo('https://public.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://secure.example.com:8080/secret.html'); - ShouldNotHaveAccessTo('https://dev.example.com:8080/groups/admin/secret.html'); - ShouldNotHaveAccessTo('https://dev.example.com:8080/groups/dev/secret.html'); - ShouldNotHaveAccessTo('https://dev.example.com:8080/users/john/secret.html'); - ShouldHaveAccessTo('https://dev.example.com:8080/users/harry/secret.html'); - ShouldNotHaveAccessTo('https://dev.example.com:8080/users/bob/secret.html'); - ShouldNotHaveAccessTo('https://admin.example.com:8080/secret.html'); - ShouldNotHaveAccessTo('https://mx1.mail.example.com:8080/secret.html'); - ShouldHaveAccessTo('https://singlefactor.example.com:8080/secret.html'); - ShouldNotHaveAccessTo('https://mx2.mail.example.com:8080/secret.html'); - }); -} \ No newline at end of file diff --git a/test/suites/HighAvailability/scenarii/AutheliaRestart.ts b/test/suites/HighAvailability/scenarii/AutheliaRestart.ts deleted file mode 100644 index 08680394e..000000000 --- a/test/suites/HighAvailability/scenarii/AutheliaRestart.ts +++ /dev/null @@ -1,52 +0,0 @@ -import Logout from "../../../helpers/Logout"; -import ChildProcess from 'child_process'; -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; -import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; -import { GET_Expect502 } from "../../../helpers/utils/Requests"; -import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; -import FullLogin from "../../../helpers/FullLogin"; -import ValidateTotp from "../../../helpers/ValidateTotp"; -import VerifyUrlIs from "../../../helpers/assertions/WaitUrlIs"; - -export default function() { - describe('Session is still valid after Authelia restarts', function() { - before(async function() { - // Be sure to start fresh - ChildProcess.execSync('rm -f .authelia-interrupt'); - - this.driver = await StartDriver(); - this.secret = await LoginAndRegisterTotp(this.driver, 'john', "password", true); - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/"); - await ValidateTotp(this.driver, this.secret); - await VerifyUrlIs(this.driver, "https://home.example.com:8080/"); - - ChildProcess.execSync('touch .authelia-interrupt'); - await GET_Expect502('https://login.example.com:8080/api/state'); - await this.driver.sleep(1000); - ChildProcess.execSync('rm .authelia-interrupt'); - await this.driver.sleep(4000); - }); - - after(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - - // Be sure to cleanup - ChildProcess.execSync('rm -f .authelia-interrupt'); - }); - - it("should still access the secret after Authelia restarted", async function() { - await VisitPageAndWaitUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - await VerifySecretObserved(this.driver); - }); - - it("should still access the secret after Authelia restarted", async function() { - await Logout(this.driver); - // The user can re-authenticate with the secret. - await FullLogin(this.driver, 'john', this.secret, "https://admin.example.com:8080/secret.html"); - await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - await VerifySecretObserved(this.driver); - }); - }); -} \ No newline at end of file diff --git a/test/suites/HighAvailability/scenarii/AuthenticationRegulation.ts b/test/suites/HighAvailability/scenarii/AuthenticationRegulation.ts deleted file mode 100644 index 05541c013..000000000 --- a/test/suites/HighAvailability/scenarii/AuthenticationRegulation.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import LoginAs from "../../../helpers/LoginAs"; -import VerifyNotificationDisplayed from "../../../helpers/assertions/VerifyNotificationDisplayed"; -import VerifyIsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage"; -import ClearFieldById from "../../../helpers/behaviors/ClearFieldById"; -import {WebDriver} from "selenium-webdriver"; - -async function ClearAndLoginAgain(driver: WebDriver, username: string, password: string) { - await ClearFieldById(driver, "username"); - await LoginAs(driver, username, password); -} - -export default function() { - describe('Authelia regulates authentications when a hacker is brute forcing', function() { - this.timeout(30000); - beforeEach(async function() { - this.driver = await StartDriver(); - }); - - afterEach(async function() { - await StopDriver(this.driver); - }); - - it("should return an error message when providing correct credentials the 4th time.", async function() { - await LoginAs(this.driver, "blackhat", "bad-password"); - await VerifyNotificationDisplayed(this.driver, "Authentication failed. Check your credentials."); - await ClearAndLoginAgain(this.driver, "blackhat", "bad-password"); - await VerifyNotificationDisplayed(this.driver, "Authentication failed. Check your credentials."); - await ClearAndLoginAgain(this.driver, "blackhat", "bad-password"); - await VerifyNotificationDisplayed(this.driver, "Authentication failed. Check your credentials."); - - // when providing good credentials, the hacker is regulated and see same message as previously. - await ClearAndLoginAgain(this.driver, "blackhat", "bad-password"); - await VerifyNotificationDisplayed(this.driver, "Authentication failed. Check your credentials."); - - // Wait the regulation ban time before retrying with correct credentials. - // It should authenticate normally. - await this.driver.sleep(12000); - await ClearAndLoginAgain(this.driver, "blackhat", "password"); - await VerifyIsSecondFactorStage(this.driver); - }); - }); -} \ No newline at end of file diff --git a/test/suites/HighAvailability/scenarii/BasicAuthentication.ts b/test/suites/HighAvailability/scenarii/BasicAuthentication.ts deleted file mode 100644 index 1a2eb43da..000000000 --- a/test/suites/HighAvailability/scenarii/BasicAuthentication.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Request from 'request-promise'; - -async function GetSecret(username: string, password: string) { - return await Request('https://singlefactor.example.com:8080/secret.html', { - auth: { - username, - password - }, - rejectUnauthorized: false, - }); -} - -export default function() { - it("should retrieve secret when Proxy-Authorization header is provided", async function() { - const res = await GetSecret('john', 'password'); - if (res.indexOf('This is a very important secret!') < 0) { - throw new Error('Cannot access secret.'); - } - }); - - it("should not retrieve secret when providing bad password", async function() { - const res = await GetSecret('john', 'bad-password'); - if (res.indexOf('This is a very important secret!') >= 0) { - throw new Error('Cannot access secret.'); - } - }); - - it("should not retrieve secret when authenticating with unexisting user", async function() { - const res = await GetSecret('dontexist', 'password'); - if (res.indexOf('This is a very important secret!') >= 0) { - throw new Error('Cannot access secret.'); - } - }); -} \ No newline at end of file diff --git a/test/suites/HighAvailability/scenarii/CustomHeadersForwarded.ts b/test/suites/HighAvailability/scenarii/CustomHeadersForwarded.ts deleted file mode 100644 index 47e7f4508..000000000 --- a/test/suites/HighAvailability/scenarii/CustomHeadersForwarded.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Logout from "../../../helpers/Logout"; -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import RegisterAndLoginWith2FA from "../../../helpers/behaviors/RegisterAndLoginTwoFactor"; -import VerifyForwardedHeaderIs from "../../../helpers/assertions/VerifyForwardedHeaderIs"; -import LoginOneFactor from "../../../helpers/behaviors/LoginOneFactor"; - -export default function() { - describe("Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend", function() { - this.timeout(100000); - - describe("Headers after single factor authentication", function() { - before(async function() { - this.driver = await StartDriver(); - await LoginOneFactor(this.driver, "john", "password", "https://singlefactor.example.com:8080/headers"); - }); - - after(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - }); - - it("should see header 'Custom-Forwarded-User' set to 'john'", async function() { - await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-User', 'john'); - }); - - it("should see header 'Custom-Forwarded-Groups' set to 'dev,admin'", async function() { - await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-Groups', 'dev,admin'); - }); - }); - - describe("Headers after two factor authentication", function() { - before(async function() { - this.driver = await StartDriver(); - await RegisterAndLoginWith2FA(this.driver, "john", "password", true, "https://secure.example.com:8080/headers"); - }); - - after(async function() { - await Logout(this.driver); - await StopDriver(this.driver); - }); - - it("should see header 'Custom-Forwarded-User' set to 'john'", async function() { - await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-User', 'john'); - }); - - it("should see header 'Custom-Forwarded-Groups' set to 'dev,admin'", async function() { - await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-Groups', 'dev,admin'); - }); - }); - }); -} \ No newline at end of file diff --git a/test/suites/HighAvailability/scenarii/EnforceInternalRedirectionsOnly.ts b/test/suites/HighAvailability/scenarii/EnforceInternalRedirectionsOnly.ts deleted file mode 100644 index 36112213b..000000000 --- a/test/suites/HighAvailability/scenarii/EnforceInternalRedirectionsOnly.ts +++ /dev/null @@ -1,72 +0,0 @@ -import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; -import ValidateTotp from "../../../helpers/ValidateTotp"; -import VerifyIsAlreadyAuthenticatedStage from "../../../helpers/assertions/VerifyIsAlreadyAuthenticatedStage"; -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; -import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs"; -import Logout from "../../../helpers/Logout"; - -/* - * Authelia should not be vulnerable to open redirection. Otherwise it would aid an - * attacker in conducting a phishing attack. - * - * To avoid the issue, Authelia's client scans the URL and prevent any redirection if - * the URL is pointing to an external domain. - */ -export default function () { - describe("Only redirection to a subdomain of the protected domain should be allowed", function () { - this.timeout(10000); - let secret: string; - - beforeEach(async function () { - this.driver = await StartDriver(); - secret = await LoginAndRegisterTotp(this.driver, "john", "password", true) - }); - - afterEach(async function () { - await Logout(this.driver); - await StopDriver(this.driver); - }) - - function CannotRedirectTo(url: string, twoFactor: boolean = true) { - it(`should redirect to already authenticated page when requesting ${url}`, async function () { - await VisitPageAndWaitUrlIs(this.driver, `https://login.example.com:8080/#/?rd=${url}`); - await ValidateTotp(this.driver, secret); - await VerifyIsAlreadyAuthenticatedStage(this.driver); - }); - } - - function CanRedirectTo(url: string) { - it(`should redirect to ${url}`, async function () { - await VisitPageAndWaitUrlIs(this.driver, `https://login.example.com:8080/#/?rd=${url}`); - await ValidateTotp(this.driver, secret); - await VerifyUrlIs(this.driver, url); - }); - } - - describe('Cannot redirect to https://www.google.fr', function () { - // Do not redirect to another domain than example.com - CannotRedirectTo("https://www.google.fr"); - }); - - describe('Cannot redirect to https://public.example.com.a:8080/secret.html', function () { - // Do not redirect to another domain than example.com - CannotRedirectTo("https://public.example.com.a:8080/secret.html"); - }); - - describe('Cannot redirect to http://secure.example.com:8080/secret.html', function () { - // Do not redirect to http website - CannotRedirectTo("http://secure.example.com:8080/secret.html"); - }); - - describe('Cannot redirect to http://singlefactor.example.com:8080/secret.html', function () { - // Do not redirect to http website - CannotRedirectTo("http://singlefactor.example.com:8080/secret.html", false); - }); - - describe('Can redirect to https://secure.example.com:8080/secret.html', function () { - // Can redirect to any subdomain of the domain protected by Authelia. - CanRedirectTo("https://secure.example.com:8080/secret.html"); - }); - }); -} \ No newline at end of file diff --git a/test/suites/HighAvailability/scenarii/MariaConnectionRecovery.ts b/test/suites/HighAvailability/scenarii/MariaConnectionRecovery.ts deleted file mode 100644 index f066cae90..000000000 --- a/test/suites/HighAvailability/scenarii/MariaConnectionRecovery.ts +++ /dev/null @@ -1,24 +0,0 @@ -import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; -import FullLogin from "../../../helpers/FullLogin"; -import WithDriver from "../../../helpers/context/WithDriver"; -import Logout from "../../../helpers/Logout"; -import { composeFiles } from '../environment'; -import DockerCompose from "../../../helpers/context/DockerCompose"; -import sleep from "../../../helpers/utils/sleep"; - -export default function () { - const dockerCompose = new DockerCompose(composeFiles); - - WithDriver(); - - it.only("should be able to login after mariadb restarts", async function () { - this.timeout(30000); - - const secret = await LoginAndRegisterTotp(this.driver, "john", "password", true); - await dockerCompose.restart('mariadb'); - await sleep(2000); - - await Logout(this.driver); - await FullLogin(this.driver, "john", secret, "https://admin.example.com:8080/secret.html"); - }); -} \ No newline at end of file diff --git a/test/suites/HighAvailability/test.ts b/test/suites/HighAvailability/test.ts deleted file mode 100644 index f744eb0d9..000000000 --- a/test/suites/HighAvailability/test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import AutheliaSuite from "../../helpers/context/AutheliaSuite"; -import MariaConnectionRecovery from "./scenarii/MariaConnectionRecovery"; -import EnforceInternalRedirectionsOnly from "./scenarii/EnforceInternalRedirectionsOnly"; -import AccessControl from "./scenarii/AccessControl"; -import CustomHeadersForwarded from "./scenarii/CustomHeadersForwarded"; -import BasicAuthentication from "./scenarii/BasicAuthentication"; -import AutheliaRestart from "./scenarii/AutheliaRestart"; -import AuthenticationRegulation from "./scenarii/AuthenticationRegulation"; - -AutheliaSuite(__dirname, function () { - this.timeout(10000); - - describe('Custom headers forwarded to backend', CustomHeadersForwarded); - describe('Access control', AccessControl); - describe('Mariadb broken connection recovery', MariaConnectionRecovery); - describe('Enforce internal redirections only', EnforceInternalRedirectionsOnly); - describe('Basic authentication', BasicAuthentication); - describe('Authelia restart', AutheliaRestart); - describe('Authentication regulation', AuthenticationRegulation); -}); \ No newline at end of file diff --git a/test/suites/NetworkACL/README.md b/test/suites/NetworkACL/README.md deleted file mode 100644 index 46312ec24..000000000 --- a/test/suites/NetworkACL/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Network ACL suite - -This suite has been created to test Authelia with basic feature in a non highly-available setup. -Authelia basically use an in-memory cache to store user sessions and persist data on disk instead -of using a remote database. Also, the user accounts are stored in file-based database. - -## Components - -Authelia, nginx, fake webmail for registering devices. - -## Tests - -Broad range of tests. \ No newline at end of file diff --git a/test/suites/NetworkACL/scenarii/NetworkACLs.ts b/test/suites/NetworkACL/scenarii/NetworkACLs.ts deleted file mode 100644 index acff0d81a..000000000 --- a/test/suites/NetworkACL/scenarii/NetworkACLs.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; -import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick"; -import ValidateTotp from "../../../helpers/ValidateTotp"; -import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; -import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; -import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs"; -import VisitPage from "../../../helpers/VisitPage"; - -async function createClient(id: number) { - return await StartDriver({ - proxyType: "manual", - httpProxy: `http://proxy-client${id}.example.com:3128` - }); -} - -export default function() { - before(async function() { - const driver = await StartDriver(); - this.secret = await LoginAndRegisterTotp(driver, "john", "password", true); - if (!this.secret) throw new Error('No secret!'); - await StopDriver(driver); - }); - - describe("Standard client (from public network)", function() { - before(async function() { - this.driver = await StartDriver(); - }); - - after(async function() { - await StopDriver(this.driver); - }); - - it("should require two factor", async function() { - await VisitPage(this.driver, "https://secure.example.com:8080/secret.html"); - await VerifyUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://secure.example.com:8080/secret.html"); - await FillLoginPageAndClick(this.driver, "john", "password"); - await ValidateTotp(this.driver, this.secret); - await VerifyUrlIs(this.driver, "https://secure.example.com:8080/secret.html"); - await VerifySecretObserved(this.driver); - }); - }) - - describe("Client 1 (from network 192.168.240.201/32)", function() { - before(async function() { - this.client1 = await createClient(1); - }); - - after(async function() { - await StopDriver(this.client1); - }); - - it("should require one factor", async function() { - await VisitPage(this.client1, "https://secure.example.com:8080/secret.html"); - await VerifyUrlIs(this.client1, "https://login.example.com:8080/#/?rd=https://secure.example.com:8080/secret.html"); - await FillLoginPageAndClick(this.client1, 'john', 'password'); - await VerifyUrlIs(this.client1, "https://secure.example.com:8080/secret.html"); - await VerifySecretObserved(this.client1); - }); - }); - - describe("Client 2 (from network 192.168.240.202/32)", function() { - before(async function() { - this.client2 = await createClient(2); - }); - - after(async function() { - await StopDriver(this.client2); - }); - - it("should bypass", async function() { - await VisitPageAndWaitUrlIs(this.client2, "https://secure.example.com:8080/secret.html"); - await VerifyUrlIs(this.client2, "https://secure.example.com:8080/secret.html"); - await VerifySecretObserved(this.client2); - }); - }); -} \ No newline at end of file diff --git a/test/suites/NetworkACL/test.ts b/test/suites/NetworkACL/test.ts deleted file mode 100644 index 4547d5188..000000000 --- a/test/suites/NetworkACL/test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import AutheliaSuite from "../../helpers/context/AutheliaSuite"; -import { exec } from '../../helpers/utils/exec'; -import NetworkACLs from "./scenarii/NetworkACLs"; - -AutheliaSuite(__dirname, function () { - this.timeout(10000); - - beforeEach(async function () { - await exec(`cp ${__dirname}/../../../internal/suites/NetworkACL/users.yml /tmp/authelia/suites/NetworkACL/users.yml`); - }); - - describe("Network ACLs", NetworkACLs); -}); \ No newline at end of file diff --git a/test/suites/ShortTimeouts/README.md b/test/suites/ShortTimeouts/README.md deleted file mode 100644 index 049cd5745..000000000 --- a/test/suites/ShortTimeouts/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Short timeouts suite - -This suite has been created to configure Authelia with short timeouts for sessions expiration -in order to test the inactivity feature and the remember me feature. - -## Components - -Authelia, nginx and a fake webmail for registering a device. - -## Tests - -Related to user inactivity. \ No newline at end of file diff --git a/test/suites/ShortTimeouts/scenarii/Inactivity.ts b/test/suites/ShortTimeouts/scenarii/Inactivity.ts deleted file mode 100644 index aba3c991b..000000000 --- a/test/suites/ShortTimeouts/scenarii/Inactivity.ts +++ /dev/null @@ -1,71 +0,0 @@ -import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; -import ValidateTotp from "../../../helpers/ValidateTotp"; -import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; -import VisitPage from "../../../helpers/VisitPage"; -import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs"; -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import ClickOnLink from "../../../helpers/ClickOnLink"; -import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick"; - -export default function(this: Mocha.ISuiteCallbackContext) { - this.timeout(20000); - - beforeEach(async function() { - this.driver = await StartDriver(); - }); - - afterEach(async function() { - await StopDriver(this.driver); - }) - - describe('Remember me not checked', function() { - beforeEach(async function() { - this.secret = await LoginAndRegisterTotp(this.driver, "john", "password", true); - }); - - it("should disconnect user after inactivity period", async function() { - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://admin.example.com:8080/secret.html"); - await ValidateTotp(this.driver, this.secret); - await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - await VisitPageAndWaitUrlIs(this.driver, "https://home.example.com:8080/"); - await this.driver.sleep(6000); - await VisitPage(this.driver, "https://admin.example.com:8080/secret.html"); - await VerifyUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://admin.example.com:8080/secret.html"); - }); - - it('should disconnect user after cookie expiration', async function() { - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://admin.example.com:8080/secret.html"); - await ValidateTotp(this.driver, this.secret); - await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - - await this.driver.sleep(2000); - await VisitPageAndWaitUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - await this.driver.sleep(2000); - await VisitPageAndWaitUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - await this.driver.sleep(2000); - await VisitPageAndWaitUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - await this.driver.sleep(2000); - await VisitPage(this.driver, "https://admin.example.com:8080/secret.html"); - await VerifyUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://admin.example.com:8080/secret.html"); - }); - }); - - describe('With remember me checkbox checked', function() { - beforeEach(async function() { - this.secret = await LoginAndRegisterTotp(this.driver, "john", "password", true); - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/"); - await ClickOnLink(this.driver, "Logout"); - }); - - it("should keep user logged in after inactivity period", async function() { - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://admin.example.com:8080/secret.html"); - await FillLoginPageAndClick(this.driver, "john", "password", true); - await ValidateTotp(this.driver, this.secret); - await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - await VisitPageAndWaitUrlIs(this.driver, "https://home.example.com:8080/#/"); - await this.driver.sleep(9000); - await VisitPage(this.driver, "https://admin.example.com:8080/secret.html"); - await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - }); - }); -} \ No newline at end of file diff --git a/test/suites/ShortTimeouts/test.ts b/test/suites/ShortTimeouts/test.ts deleted file mode 100644 index 0ec0498e1..000000000 --- a/test/suites/ShortTimeouts/test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import AutheliaSuite from "../../helpers/context/AutheliaSuite"; -import Inactivity from './scenarii/Inactivity'; -import AuthenticationBlacklisting from "../../helpers/scenarii/AuthenticationBlacklisting"; - -AutheliaSuite(__dirname, function () { - this.timeout(10000); - describe('Inactivity period', Inactivity); - describe('Authentication blacklisting', AuthenticationBlacklisting(10000)); -}); \ No newline at end of file diff --git a/test/suites/Standalone/scenarii/AlreadyLoggedIn.ts b/test/suites/Standalone/scenarii/AlreadyLoggedIn.ts deleted file mode 100644 index 937f2b467..000000000 --- a/test/suites/Standalone/scenarii/AlreadyLoggedIn.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import VisitPage from "../../../helpers/VisitPage"; -import VerifyIsAlreadyAuthenticatedStage from "../../../helpers/assertions/VerifyIsAlreadyAuthenticatedStage"; -import RegisterAndLoginTwoFactor from "../../../helpers/behaviors/RegisterAndLoginTwoFactor"; - - -export default function() { - describe('When visiting https://login.example.com:8080/#/ while authenticated, the user is redirected to already logged in page', function() { - before(async function() { - this.driver = await StartDriver(); - await RegisterAndLoginTwoFactor(this.driver, 'john', "password", true); - }); - - after(async function() { - await StopDriver(this.driver); - }); - - it('should redirect the user', async function() { - await VisitPage(this.driver, 'https://login.example.com:8080/#/'); - await VerifyIsAlreadyAuthenticatedStage(this.driver); - }); - }); -} \ No newline at end of file diff --git a/test/suites/Standalone/scenarii/BackendProtection.ts b/test/suites/Standalone/scenarii/BackendProtection.ts deleted file mode 100644 index bb32e777e..000000000 --- a/test/suites/Standalone/scenarii/BackendProtection.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { POST_Expect403, GET_Expect403 } from "../../../helpers/utils/Requests"; - -export default function() { - // POST - it('should return 403 error when posting to https://login.example.com:8080/api/secondfactor/totp', async function() { - await POST_Expect403('https://login.example.com:8080/api/secondfactor/totp', { token: 'MALICIOUS_TOKEN' }); - }); - - it('should return 403 error when posting to https://login.example.com:8080/api/secondfactor/u2f/sign', async function() { - await POST_Expect403('https://login.example.com:8080/api/secondfactor/u2f/sign'); - }); - - it('should return 403 error when posting to https://login.example.com:8080/api/secondfactor/u2f/register', async function() { - await POST_Expect403('https://login.example.com:8080/api/secondfactor/u2f/register'); - }); - - it('should return 403 error on GET to https://login.example.com:8080/api/secondfactor/u2f/sign_request', async function() { - await POST_Expect403('https://login.example.com:8080/api/secondfactor/u2f/sign_request'); - }); - - it('should return 403 error when posting to https://login.example.com:8080/api/secondfactor/preferences', async function() { - await POST_Expect403('https://login.example.com:8080/api/secondfactor/preferences'); - }); - - it('should return 403 error on GET to https://login.example.com:8080/api/secondfactor/preferences', async function() { - await GET_Expect403('https://login.example.com:8080/api/secondfactor/preferences'); - }); - - it('should return 403 error on GET to https://login.example.com:8080/api/secondfactor/available', async function() { - await GET_Expect403('https://login.example.com:8080/api/secondfactor/available'); - }); - - - describe('Identity validation endpoints blocked to unauthenticated users', function() { - it('should return 403 error on POST to https://login.example.com:8080/api/secondfactor/u2f/identity/start', async function() { - await POST_Expect403('https://login.example.com:8080/api/secondfactor/u2f/identity/start'); - }); - - it('should return 403 error on POST to https://login.example.com:8080/api/secondfactor/u2f/identity/finish', async function() { - await POST_Expect403('https://login.example.com:8080/api/secondfactor/u2f/identity/finish'); - }); - - it('should return 403 error on POST to https://login.example.com:8080/api/secondfactor/totp/identity/start', async function() { - await POST_Expect403('https://login.example.com:8080/api/secondfactor/totp/identity/start'); - }); - - it('should return 403 error on POST to https://login.example.com:8080/api/secondfactor/totp/identity/finish', async function() { - await POST_Expect403('https://login.example.com:8080/api/secondfactor/totp/identity/finish'); - }); - }); -} \ No newline at end of file diff --git a/test/suites/Standalone/scenarii/BadPassword.ts b/test/suites/Standalone/scenarii/BadPassword.ts deleted file mode 100644 index eeac44fa4..000000000 --- a/test/suites/Standalone/scenarii/BadPassword.ts +++ /dev/null @@ -1,28 +0,0 @@ -import FillLoginPageWithUserAndPasswordAndClick from '../../../helpers/FillLoginPageAndClick'; -import VisitPageAndWaitUrlIs from '../../../helpers/behaviors/VisitPageAndWaitUrlIs'; -import VerifyNotificationDisplayed from '../../../helpers/assertions/VerifyNotificationDisplayed'; -import { StartDriver, StopDriver } from '../../../helpers/context/WithDriver'; - -export default function() { -/** - * When user provides bad password, - * Then he gets a notification message. - */ - describe('failed login as john in first factor', function() { - this.timeout(10000); - - before(async function() { - this.driver = await StartDriver(); - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/") - await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'bad_password'); - }); - - after(async function() { - await StopDriver(this.driver); - }) - - it('should get a notification message', async function () { - await VerifyNotificationDisplayed(this.driver, "Authentication failed. Check your credentials."); - }); - }); -} diff --git a/test/suites/Standalone/scenarii/BypassPolicy.ts b/test/suites/Standalone/scenarii/BypassPolicy.ts deleted file mode 100644 index f63dbe75a..000000000 --- a/test/suites/Standalone/scenarii/BypassPolicy.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; -import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; - -export default function() { - before(async function() { - this.driver = await StartDriver(); - }); - - after(async function () { - await StopDriver(this.driver); - }); - - it('should have access to public.example.com/secret.html', async function () { - await VisitPageAndWaitUrlIs(this.driver, "https://public.example.com:8080/secret.html"); - await VerifySecretObserved(this.driver); - }); -} \ No newline at end of file diff --git a/test/suites/Standalone/scenarii/NoDuoPushOption.ts b/test/suites/Standalone/scenarii/NoDuoPushOption.ts deleted file mode 100644 index 4ab0a7ea6..000000000 --- a/test/suites/Standalone/scenarii/NoDuoPushOption.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import LoginAs from "../../../helpers/LoginAs"; -import VerifyIsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage"; -import ClickOnLink from "../../../helpers/ClickOnLink"; -import VerifyIsUseAnotherMethodView from "../../../helpers/assertions/VerifyIsUseAnotherMethodView"; -import VerifyButtonDoesNotExist from "../../../helpers/assertions/VerifyButtonDoesNotExist"; -import VerifyButtonHasAppeared from "../../../helpers/assertions/VerifyButtonHasAppeared"; - - - -export default function() { - before(async function() { - this.driver = await StartDriver(); - }); - - after(async function() { - await StopDriver(this.driver); - }); - - // The Duo API is not configured so we should not see the method. - it("should not display duo push notification method", async function() { - await LoginAs(this.driver, "john", "password", "https://secure.example.com:8080/"); - await VerifyIsSecondFactorStage(this.driver); - - await ClickOnLink(this.driver, 'Use another method'); - await VerifyIsUseAnotherMethodView(this.driver); - await VerifyButtonHasAppeared(this.driver, "One-Time Password"); - await VerifyButtonDoesNotExist(this.driver, "Duo Push Notification"); - }); -} \ No newline at end of file diff --git a/test/suites/Standalone/scenarii/RegisterTotp.ts b/test/suites/Standalone/scenarii/RegisterTotp.ts deleted file mode 100644 index 4aa465e7c..000000000 --- a/test/suites/Standalone/scenarii/RegisterTotp.ts +++ /dev/null @@ -1,51 +0,0 @@ -import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; -import Assert from 'assert'; -import LoginAndRegisterTotp from '../../../helpers/LoginAndRegisterTotp'; -import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; -import sleep from "../../../helpers/utils/sleep"; - -/** - * Given the user logs in as john, - * When he register a TOTP token, - * Then he reach a page containing the secret as string an qrcode - */ -export default function() { - describe('successfully login as john', function() { - this.timeout(10000); - - before(async function() { - this.driver = await StartDriver(); - await LoginAndRegisterTotp(this.driver, "john", "password", true); - }); - - after(async function() { - await StopDriver(this.driver); - }) - - it("should see generated qrcode", async function() { - await this.driver.wait( - SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.className("qrcode")), - 5000); - }); - - it("should see generated secret", async function() { - await this.driver.wait( - SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.className("base32-secret")), - 5000); - }); - - it("should have user and issuer in otp url", async function() { - const el = await (this.driver as WebDriver).wait( - SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.className('otpauth-secret')), 5000); - - const otpauthUrl = await el.getAttribute('innerText'); - const label = 'john'; - const issuer = 'example.com'; - - Assert(new RegExp(`^otpauth://totp/${issuer}:${label}\\?algorithm=SHA1&digits=6&issuer=${issuer}&period=30&secret=[A-Z0-9]+$`).test(otpauthUrl)); - }) - }); -}; diff --git a/test/suites/Standalone/scenarii/ResetPassword.ts b/test/suites/Standalone/scenarii/ResetPassword.ts deleted file mode 100644 index 77d2cc3ac..000000000 --- a/test/suites/Standalone/scenarii/ResetPassword.ts +++ /dev/null @@ -1,64 +0,0 @@ -import SeleniumWebDriver from 'selenium-webdriver'; - -import ClickOnLink from '../../../helpers/ClickOnLink'; -import ClickOn from '../../../helpers/ClickOn'; -import FillField from "../../../helpers/FillField"; -import {GetLinkFromEmail} from "../../../helpers/GetIdentityLink"; -import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick"; -import IsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage"; -import VisitPageAndWaitUrlIs from '../../../helpers/behaviors/VisitPageAndWaitUrlIs'; -import VerifyNotificationDisplayed from '../../../helpers/assertions/VerifyNotificationDisplayed'; -import VerifyUrlIs from '../../../helpers/assertions/VerifyUrlIs'; -import { StartDriver, StopDriver } from '../../../helpers/context/WithDriver'; -import ResetPassword from '../../../helpers/behaviors/ResetPassword'; - -export default function() { - beforeEach(async function() { - this.driver = await StartDriver(); - }); - - afterEach(async function() { - await StopDriver(this.driver); - }) - - it("should reset password for john", async function() { - await ResetPassword(this.driver, "john", "newpass"); - await VerifyUrlIs(this.driver, "https://login.example.com:8080/#/"); - await FillLoginPageAndClick(this.driver, "john", "newpass"); - - // The user reaches the second factor page using the new password. - await IsSecondFactorStage(this.driver); - - // restore password - await ClickOnLink(this.driver, "Logout"); - await ResetPassword(this.driver, "john", "password"); - }); - - it("should make attacker think reset password is initiated", async function() { - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/"); - await ClickOnLink(this.driver, "Forgot password\?"); - await VerifyUrlIs(this.driver, "https://login.example.com:8080/#/forgot-password"); - await FillField(this.driver, "username", "unknown"); - await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button')); - - // The malicious user thinks the confirmation has been sent. - await VerifyUrlIs(this.driver, 'https://login.example.com:8080/#/confirmation-sent'); - }); - - it("should notify passwords are different in reset form", async function() { - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/"); - await ClickOnLink(this.driver, "Forgot password\?"); - await VerifyUrlIs(this.driver, "https://login.example.com:8080/#/forgot-password"); - await FillField(this.driver, "username", "john"); - await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button')); - await VerifyUrlIs(this.driver, 'https://login.example.com:8080/#/confirmation-sent'); - - await this.driver.sleep(500); // Simulate the time it takes to receive the e-mail. - const link = await GetLinkFromEmail(); - await VisitPageAndWaitUrlIs(this.driver, link); - await FillField(this.driver, "password1", "newpass"); - await FillField(this.driver, "password2", "badpass"); - await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button')); - await VerifyNotificationDisplayed(this.driver, "The passwords are different."); - }); -} diff --git a/test/suites/Standalone/scenarii/TOTPValidation.ts b/test/suites/Standalone/scenarii/TOTPValidation.ts deleted file mode 100644 index 24f77f15b..000000000 --- a/test/suites/Standalone/scenarii/TOTPValidation.ts +++ /dev/null @@ -1,62 +0,0 @@ -import FillLoginPageWithUserAndPasswordAndClick from '../../../helpers/FillLoginPageAndClick'; -import ValidateTotp from '../../../helpers/ValidateTotp'; -import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; -import LoginAndRegisterTotp from '../../../helpers/LoginAndRegisterTotp'; -import VisitPageAndWaitUrlIs from '../../../helpers/behaviors/VisitPageAndWaitUrlIs'; -import VerifyNotificationDisplayed from '../../../helpers/assertions/VerifyNotificationDisplayed'; -import VerifyUrlIs from '../../../helpers/assertions/VerifyUrlIs'; -import { StartDriver, StopDriver } from '../../../helpers/context/WithDriver'; - -export default function() { - /** - * Given john has registered a TOTP secret, - * When he validates the TOTP second factor, - * Then he has access to secret page. - */ - describe('Successfully pass second factor with TOTP', function() { - before(async function() { - this.driver = await StartDriver(); - const secret = await LoginAndRegisterTotp(this.driver, "john", "password", true); - if (!secret) throw new Error('No secret!'); - - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://admin.example.com:8080/secret.html"); - await ValidateTotp(this.driver, secret); - }); - - after(async function() { - await StopDriver(this.driver); - }); - - it("should be automatically redirected to secret page", async function() { - await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - }); - - it("should access the secret", async function() { - await VerifySecretObserved(this.driver); - }); - }); - - /** - * Given john has registered a TOTP secret, - * When he fails the TOTP challenge, - * Then he gets a notification message. - */ - describe('Fail validation of second factor with TOTP', function() { - before(async function() { - this.driver = await StartDriver(); - await LoginAndRegisterTotp(this.driver, "john", "password", true); - const BAD_TOKEN = "125478"; - - await VisitPageAndWaitUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://admin.example.com:8080/secret.html"); - await ValidateTotp(this.driver, BAD_TOKEN); - }); - - after(async function() { - await StopDriver(this.driver); - }); - - it("get a notification message", async function() { - await VerifyNotificationDisplayed(this.driver, "Authentication failed, please retry later."); - }); - }); -} diff --git a/test/suites/Standalone/scenarii/VerifyEndpoint.ts b/test/suites/Standalone/scenarii/VerifyEndpoint.ts deleted file mode 100644 index 6a9ed1f97..000000000 --- a/test/suites/Standalone/scenarii/VerifyEndpoint.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { GET_Expect401, GET_ExpectRedirect } from "../../../helpers/utils/Requests"; - -export default function() { - describe('Query without authenticated cookie', function() { - it('should get a 401 on GET to http://authelia.example.com:9091/api/verify', async function() { - await GET_Expect401('http://authelia.example.com:9091/api/verify', { - 'X-Forwarded-Proto': 'https', - }); - }); - - describe('Kubernetes nginx ingress controller', async function() { - it('should redirect to https://login.example.com:8080', async function() { - await GET_ExpectRedirect('http://authelia.example.com:9091/api/verify?rd=https://login.example.com:8080/%23/', - 'https://login.example.com:8080/#/?rd=https://secure.example.com:8080/', - { - 'X-Original-Url': 'https://secure.example.com:8080/', - 'X-Forwarded-Proto': 'https' - }); - }); - }); - - describe('Traefik proxy', async function() { - it('should redirect to https://login.example.com:8080', async function() { - await GET_ExpectRedirect('http://authelia.example.com:9091/api/verify?rd=https://login.example.com:8080/%23/', - 'https://login.example.com:8080/#/?rd=https://secure.example.com:8080/', - { - 'X-Forwarded-Proto': 'https', - 'X-Forwarded-Host': 'secure.example.com:8080', - 'X-Forwarded-Uri': '/', - }); - }); - }); - }); -} \ No newline at end of file diff --git a/test/suites/Standalone/test.ts b/test/suites/Standalone/test.ts deleted file mode 100644 index 056a07866..000000000 --- a/test/suites/Standalone/test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import AutheliaSuite from "../../helpers/context/AutheliaSuite"; -import BadPassword from "./scenarii/BadPassword"; -import RegisterTotp from './scenarii/RegisterTotp'; -import ResetPassword from './scenarii/ResetPassword'; -import TOTPValidation from './scenarii/TOTPValidation'; -import BackendProtection from './scenarii/BackendProtection'; -import VerifyEndpoint from './scenarii/VerifyEndpoint'; -import AlreadyLoggedIn from './scenarii/AlreadyLoggedIn'; -import { exec } from '../../helpers/utils/exec'; -import BypassPolicy from "./scenarii/BypassPolicy"; -import NoDuoPushOption from "./scenarii/NoDuoPushOption"; - -AutheliaSuite("/tmp/authelia/suites/Standalone/", function() { - this.timeout(10000); - - beforeEach(async function() { - await exec(`cp ${__dirname}/../../../internal/suites/Standalone/users.yml /tmp/authelia/suites/Standalone/users.yml`); - }); - - describe('Bypass policy', BypassPolicy) - describe('Backend protection', BackendProtection); - describe('Verify API endpoint', VerifyEndpoint); - describe('Bad password', BadPassword); - describe('Reset password', ResetPassword); - describe('TOTP Registration', RegisterTotp); - describe('TOTP Validation', TOTPValidation); - describe('Already logged in', AlreadyLoggedIn); - describe('No Duo Push method available', NoDuoPushOption); -}); \ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json deleted file mode 100644 index 66fabf625..000000000 --- a/test/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "allowJs": true, - "skipLibCheck": false, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "commonjs", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": false, - "noEmit": true, - "lib": [ - "esnext" - ] - } -} diff --git a/web/src/components/FailureIcon.tsx b/web/src/components/FailureIcon.tsx index 4cb8f50ff..7463628be 100644 --- a/web/src/components/FailureIcon.tsx +++ b/web/src/components/FailureIcon.tsx @@ -6,6 +6,6 @@ export interface Props { } export default function (props: Props) { return ( - + ) } \ No newline at end of file diff --git a/web/src/components/NotificationBar.tsx b/web/src/components/NotificationBar.tsx index 315a6f1cb..f426cab4b 100644 --- a/web/src/components/NotificationBar.tsx +++ b/web/src/components/NotificationBar.tsx @@ -28,6 +28,7 @@ export default function (props: Props) { onClose={props.onClose} onExited={() => setTmpNotification(null)}> diff --git a/web/src/components/SuccessIcon.tsx b/web/src/components/SuccessIcon.tsx index 55bf79414..30c263fc0 100644 --- a/web/src/components/SuccessIcon.tsx +++ b/web/src/components/SuccessIcon.tsx @@ -6,6 +6,6 @@ export interface Props { } export default function (props: Props) { return ( - + ) } \ No newline at end of file diff --git a/web/src/layouts/LoginLayout.tsx b/web/src/layouts/LoginLayout.tsx index 7e2444216..78c20eecd 100644 --- a/web/src/layouts/LoginLayout.tsx +++ b/web/src/layouts/LoginLayout.tsx @@ -5,6 +5,7 @@ import { grey } from "@material-ui/core/colors"; export interface Props { + id?: string; children?: ReactNode; title: string; showBrand?: boolean; @@ -14,6 +15,7 @@ export default function (props: Props) { const style = useStyles(); return ( : null} + {secretBase32 + ? : null} {" | "} - + + {" | "} + createErrorNotification(err.message)} @@ -108,6 +115,7 @@ export default function (props: Props) { createErrorNotification(err.message)} @@ -115,6 +123,7 @@ export default function (props: Props) { createErrorNotification(err.message)} onSignInSuccess={props.onAuthenticationSuccess} /> diff --git a/web/src/views/LoginPortal/SecondFactor/SecurityKeyMethod.tsx b/web/src/views/LoginPortal/SecondFactor/SecurityKeyMethod.tsx index 526b75ff1..8ef67f0d9 100644 --- a/web/src/views/LoginPortal/SecondFactor/SecurityKeyMethod.tsx +++ b/web/src/views/LoginPortal/SecondFactor/SecurityKeyMethod.tsx @@ -22,6 +22,7 @@ export enum State { } export interface Props { + id: string; authenticationLevel: AuthenticationLevel; onRegisterClick: () => void; @@ -91,6 +92,7 @@ export default function (props: Props) { return ( diff --git a/web/src/views/LoginPortal/SignOut/SignOut.tsx b/web/src/views/LoginPortal/SignOut/SignOut.tsx index 5c06854fb..ba87e47b4 100644 --- a/web/src/views/LoginPortal/SignOut/SignOut.tsx +++ b/web/src/views/LoginPortal/SignOut/SignOut.tsx @@ -6,11 +6,12 @@ import { Typography, makeStyles } from "@material-ui/core"; import { Redirect } from "react-router"; import { FirstFactorRoute } from "../../../Routes"; import { useRedirectionURL } from "../../../hooks/RedirectionURL"; +import { useIsMountedRef } from "../../../hooks/Mounted"; -export interface Props { -} +export interface Props { } export default function (props: Props) { + const mounted = useIsMountedRef(); const style = useStyles(); const { createErrorNotification } = useNotifications(); const redirectionURL = useRedirectionURL(); @@ -20,7 +21,12 @@ export default function (props: Props) { try { // TODO(c.michaud): pass redirection URL to backend for validation. await signOut(); - setTimeout(() => { setTimedOut(true); }, 2000); + setTimeout(() => { + if (!mounted) { + return; + } + setTimedOut(true); + }, 2000); } catch (err) { console.error(err); createErrorNotification("There was an issue signing out"); diff --git a/web/src/views/ResetPassword/ResetPasswordStep1.tsx b/web/src/views/ResetPassword/ResetPasswordStep1.tsx index 6b6f8b9c7..14f05d12e 100644 --- a/web/src/views/ResetPassword/ResetPasswordStep1.tsx +++ b/web/src/views/ResetPassword/ResetPasswordStep1.tsx @@ -22,9 +22,9 @@ export default function () { try { await initiateResetPasswordProcess(username); - createInfoNotification("An email has been sent to your address to complete the process"); + createInfoNotification("An email has been sent to your address to complete the process."); } catch (err) { - createErrorNotification("There was an issue initiating the password reset process"); + createErrorNotification("There was an issue initiating the password reset process."); } } @@ -37,10 +37,11 @@ export default function () { } return ( - +