diff --git a/Jenkinsfile.ui b/Jenkinsfile.ui new file mode 100644 index 000000000..8f1348689 --- /dev/null +++ b/Jenkinsfile.ui @@ -0,0 +1,78 @@ +pipeline { + agent { + docker { + label 'main' + image docker.build("storj-ci", "--pull git://github.com/storj/ci.git#main").id + args '-u root:root --cap-add SYS_PTRACE -v "/tmp/gomod":/go/pkg/mod -v "/tmp/npm":/npm --tmpfs "/tmp:exec,mode=777"' + } + } + options { + timeout(time: 36, unit: 'MINUTES') + } + environment { + NPM_CONFIG_CACHE = '/npm/cache' + GOTRACEBACK = 'all' + COCKROACH_MEMPROF_INTERVAL=0 + } + stages { + stage('Build') { + steps { + checkout scm + + sh 'mkdir -p .build' + + sh 'go mod download' + sh 'service postgresql start' + + dir(".build") { + sh 'cockroach start-single-node --insecure --store=type=mem,size=2GiB --listen-addr=localhost:26256 --http-addr=localhost:8086 --cache 512MiB --max-sql-memory 512MiB --background' + } + } + } + + + stage('Verification') { + stage('UI') { + environment { + STORJ_TEST_COCKROACH = 'cockroach://root@localhost:26256/testui?sslmode=disable' + STORJ_TEST_POSTGRES = 'postgres://postgres@localhost/testui?sslmode=disable' + STORJ_TEST_BROWSER = '/usr/bin/chromium' + STORJ_TEST_SATELLITE_WEB = "${pwd()}/.build/satellite-web" + + DISPLAY = ':99' + } + steps { + sh 'cockroach sql --insecure --host=localhost:26256 -e \'create database testui;\'' + sh 'psql -U postgres -c \'create database testui;\'' + + sh 'cp -r ./web/satellite/ ${STORJ_TEST_SATELLITE_WEB}/' + + // TODO: this is not quite correct + sh 'mkdir ${STORJ_TEST_SATELLITE_WEB}/wasm' + sh 'cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ${STORJ_TEST_SATELLITE_WEB}/wasm/wasm_exec.js' + sh 'GOOS=js GOARCH=wasm go build -o ${STORJ_TEST_SATELLITE_WEB}/wasm/main.wasm storj.io/storj/satellite/console/wasm' + + sh 'cd .build/satellite-web && npm install' + sh 'cd .build/satellite-web && npm run build' + + sh 'Xvfb -ac :99 -screen 0 1280x1024x16 &' + sh 'go test -vet=off -race -json ./integration/ui/... 2>&1 | tee .build/ui-tests.json | xunit -out .build/ui-tests.xml' + } + post { + always { + sh script: 'cat .build/ui-tests.json | tparse -all -top -slow 100', returnStatus: true + archiveArtifacts artifacts: '.build/ui-tests.json' + junit '.build/ui-tests.xml' + } + } + } + } + } + } + post { + always { + sh "chmod -R 777 ." // ensure Jenkins agent can delete the working directory + deleteDir() + } + } +} diff --git a/go.mod b/go.mod index e9c7a7771..944eddee9 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/cheggaaa/pb/v3 v3.0.5 github.com/fatih/color v1.9.0 github.com/go-redis/redis/v8 v8.7.1 + github.com/go-rod/rod v0.100.0 github.com/gogo/protobuf v1.3.2 github.com/google/go-cmp v0.5.4 github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3 // indirect diff --git a/go.sum b/go.sum index d313bf0a8..7c617e034 100644 --- a/go.sum +++ b/go.sum @@ -125,6 +125,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-redis/redis/v8 v8.7.1 h1:8IYi6RO83fNcG5amcUUYTN/qH2h4OjZHlim3KWGFSsA= github.com/go-redis/redis/v8 v8.7.1/go.mod h1:BRxHBWn3pO3CfjyX6vAoyeRmCquvxr6QG+2onGV2gYs= +github.com/go-rod/rod v0.100.0 h1:tEKIb5wS3pGUpW4oJPYDxOKmRXaZbd6S+YVjJ6BHBBY= +github.com/go-rod/rod v0.100.0/go.mod h1:h9igqSGReLmOWyHtdf0AtUd0mdkHFu3gFwBeV+stleM= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -508,6 +510,16 @@ github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3/go.mod h1:R0Gbuw github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= +github.com/ysmood/goob v0.3.0 h1:XZ51cZJ4W3WCoCiUktixzMIQF86W7G5VFL4QQ/Q2uS0= +github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= +github.com/ysmood/got v0.12.0 h1:Ol4cpy6Xdq1KCjPlWSA+tvekrnt9cV6LIw+Jvx0dj4M= +github.com/ysmood/got v0.12.0/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= +github.com/ysmood/gotrace v0.2.2 h1:006KHGRThSRf8lwh4EyhNmuuq/l+Ygs+JqojkhEG1/E= +github.com/ysmood/gotrace v0.2.2/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.6.4 h1:Yb6tosv6bk59HqjZu2/7o4BFherpYEMkDkXmlhgryZ4= +github.com/ysmood/gson v0.6.4/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.7.0 h1:XCGdaPExyoreoQd+H5qgxM3ReNbSPFsEXpSKwbXbwQw= +github.com/ysmood/leakless v0.7.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= diff --git a/integration/ui/satellite/user_login_test.go b/integration/ui/satellite/user_login_test.go new file mode 100644 index 000000000..41e7f7a87 --- /dev/null +++ b/integration/ui/satellite/user_login_test.go @@ -0,0 +1,33 @@ +// Copyright (C) 2021 Storj Labs, Inc. +// See LICENSE for copying information. + +package satellite_test + +import ( + "testing" + "time" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/input" + "github.com/stretchr/testify/require" + + "storj.io/common/testcontext" + "storj.io/storj/integration/ui/uitest" + "storj.io/storj/private/testplanet" +) + +func TestLoginToAccount(t *testing.T) { + uitest.Run(t, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet, browser *rod.Browser) { + loginPageURL := planet.Satellites[0].ConsoleURL() + "/login" + user := planet.Uplinks[0].User[planet.Satellites[0].ID()] + + page := browser.Timeout(10 * time.Second).MustPage(loginPageURL) + page.MustSetViewport(1350, 600, 1, false) + page.MustElement(".headerless-input").MustInput(user.Email) + page.MustElement("[type=password]").MustInput(user.Password) + page.Keyboard.MustPress(input.Enter) + + dashboardTitle := page.MustElement(".dashboard-area__header-wrapper__title").MustText() + require.Contains(t, dashboardTitle, "Dashboard") + }) +} diff --git a/integration/ui/uitest/run.go b/integration/ui/uitest/run.go new file mode 100644 index 000000000..e2b507b74 --- /dev/null +++ b/integration/ui/uitest/run.go @@ -0,0 +1,92 @@ +// Copyright (C) 2021 Storj Labs, Inc. +// See LICENSE for copying information. + +package uitest + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/utils" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" + + "storj.io/common/testcontext" + "storj.io/storj/private/testplanet" + "storj.io/storj/satellite" +) + +// Test defines common services for uitests. +type Test func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet, browser *rod.Browser) + +type zapWriter struct { + *zap.Logger +} + +func (log zapWriter) Write(data []byte) (int, error) { + log.Logger.Info(string(data)) + return len(data), nil +} + +// Run starts a new UI test. +func Run(t *testing.T, test Test) { + if os.Getenv("STORJ_TEST_SATELLITE_WEB") == "" { + t.Skip("Enable UI tests by setting STORJ_TEST_SATELLITE_WEB to built npm") + } + if os.Getenv("STORJ_TEST_SATELLITE_WEB") == "omit" { + return + } + + testplanet.Run(t, testplanet.Config{ + SatelliteCount: 1, StorageNodeCount: 4, UplinkCount: 1, + Reconfigure: testplanet.Reconfigure{ + Satellite: func(log *zap.Logger, index int, config *satellite.Config) { + config.Console.StaticDir = os.Getenv("STORJ_TEST_SATELLITE_WEB") + }, + }, + NonParallel: true, + }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { + showBrowser := os.Getenv("STORJ_TEST_SHOW_BROWSER") != "" + + logLauncher := zaptest.NewLogger(t).Named("launcher") + + launch := launcher.New(). + Headless(!showBrowser). + Leakless(false). + Devtools(false). + NoSandbox(true). + UserDataDir(ctx.Dir("browser")). + Logger(zapWriter{Logger: logLauncher}) + + if browserBin := os.Getenv("STORJ_TEST_BROWSER"); browserBin != "" { + launch = launch.Bin(browserBin) + } + + defer launch.Cleanup() + + url, err := launch.Launch() + require.NoError(t, err) + + logBrowser := zaptest.NewLogger(t).Named("rod") + + browser := rod.New(). + Timeout(time.Minute). + ControlURL(url). + SlowMotion(300 * time.Millisecond). + Logger(utils.Log(func(msg ...interface{}) { + logBrowser.Info(fmt.Sprintln(msg...)) + })). + Context(ctx). + WithPanic(func(v interface{}) { require.Fail(t, "check failed", v) }) + defer ctx.Check(browser.Close) + + require.NoError(t, browser.Connect()) + + test(t, ctx, planet, browser) + }) +} diff --git a/integration/ui/uitest/run_test.go b/integration/ui/uitest/run_test.go new file mode 100644 index 000000000..151195482 --- /dev/null +++ b/integration/ui/uitest/run_test.go @@ -0,0 +1,20 @@ +// Copyright (C) 2021 Storj Labs, Inc. +// See LICENSE for copying information. + +package uitest_test + +import ( + "testing" + + "github.com/go-rod/rod" + + "storj.io/common/testcontext" + "storj.io/storj/integration/ui/uitest" + "storj.io/storj/private/testplanet" +) + +func TestRun(t *testing.T) { + uitest.Run(t, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet, browser *rod.Browser) { + t.Log("working") + }) +} diff --git a/private/testplanet/satellite.go b/private/testplanet/satellite.go index a55d8c1d1..4975f07e6 100644 --- a/private/testplanet/satellite.go +++ b/private/testplanet/satellite.go @@ -185,6 +185,11 @@ func (system *Satellite) Addr() string { return system.API.Server.Addr().String( // URL returns the node url from the Satellite system API. func (system *Satellite) URL() string { return system.NodeURL().String() } +// ConsoleURL returns the console URL. +func (system *Satellite) ConsoleURL() string { + return "http://" + system.API.Console.Listener.Addr().String() +} + // NodeURL returns the storj.NodeURL from the Satellite system API. func (system *Satellite) NodeURL() storj.NodeURL { return storj.NodeURL{ID: system.API.ID(), Address: system.API.Addr()} diff --git a/private/testplanet/satellite_test.go b/private/testplanet/satellite_test.go index d0ec6a07d..f4bbb3a5d 100644 --- a/private/testplanet/satellite_test.go +++ b/private/testplanet/satellite_test.go @@ -21,6 +21,7 @@ func TestSatellite_AddProject(t *testing.T) { user, err := planet.Satellites[0].AddUser(ctx, console.CreateUser{ FullName: "test user", Email: "test-email@test", + Password: "password", }, 4) require.NoError(t, err) diff --git a/private/testplanet/uplink.go b/private/testplanet/uplink.go index 2a60153c5..8ee0f21d8 100644 --- a/private/testplanet/uplink.go +++ b/private/testplanet/uplink.go @@ -39,6 +39,7 @@ type Uplink struct { APIKey map[storj.NodeID]*macaroon.APIKey Access map[storj.NodeID]*uplink.Access + User map[storj.NodeID]UserLogin // Projects is indexed by the satellite number. Projects []*Project @@ -63,6 +64,12 @@ type ProjectOwner struct { Email string } +// UserLogin contains information about the user login. +type UserLogin struct { + Email string + Password string +} + // DialMetainfo dials the satellite with the appropriate api key. func (project *Project) DialMetainfo(ctx context.Context) (*metaclient.Client, error) { return project.client.DialMetainfo(ctx, project.Satellite, project.RawAPIKey) @@ -106,6 +113,7 @@ func (planet *Planet) newUplink(ctx context.Context, name string) (*Uplink, erro Identity: identity, APIKey: map[storj.NodeID]*macaroon.APIKey{}, Access: map[storj.NodeID]*uplink.Access{}, + User: map[storj.NodeID]UserLogin{}, } planetUplink.Log.Debug("id=" + identity.ID.String()) @@ -124,6 +132,11 @@ func (planet *Planet) newUplink(ctx context.Context, name string) (*Uplink, erro return nil, err } + planetUplink.User[satellite.ID()] = UserLogin{ + Email: user.Email, + Password: user.FullName, + } + project, err := satellite.AddProject(ctx, user.ID, projectName) if err != nil { return nil, err