integration/ui: add UI integration tests
Introduce a new `integration/ui` package for integration tests. `integration/ui/uitest` contains a helper package for running browser tests.
This commit is contained in:
parent
962f81433f
commit
072dd24696
78
Jenkinsfile.ui
Normal file
78
Jenkinsfile.ui
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
go.mod
1
go.mod
@ -11,6 +11,7 @@ require (
|
|||||||
github.com/cheggaaa/pb/v3 v3.0.5
|
github.com/cheggaaa/pb/v3 v3.0.5
|
||||||
github.com/fatih/color v1.9.0
|
github.com/fatih/color v1.9.0
|
||||||
github.com/go-redis/redis/v8 v8.7.1
|
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/gogo/protobuf v1.3.2
|
||||||
github.com/google/go-cmp v0.5.4
|
github.com/google/go-cmp v0.5.4
|
||||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3 // indirect
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3 // indirect
|
||||||
|
12
go.sum
12
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-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 h1:8IYi6RO83fNcG5amcUUYTN/qH2h4OjZHlim3KWGFSsA=
|
||||||
github.com/go-redis/redis/v8 v8.7.1/go.mod h1:BRxHBWn3pO3CfjyX6vAoyeRmCquvxr6QG+2onGV2gYs=
|
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/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 h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
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/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 h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
|
||||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
|
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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/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=
|
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
|
||||||
|
33
integration/ui/satellite/user_login_test.go
Normal file
33
integration/ui/satellite/user_login_test.go
Normal file
@ -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")
|
||||||
|
})
|
||||||
|
}
|
92
integration/ui/uitest/run.go
Normal file
92
integration/ui/uitest/run.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
20
integration/ui/uitest/run_test.go
Normal file
20
integration/ui/uitest/run_test.go
Normal file
@ -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")
|
||||||
|
})
|
||||||
|
}
|
@ -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.
|
// URL returns the node url from the Satellite system API.
|
||||||
func (system *Satellite) URL() string { return system.NodeURL().String() }
|
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.
|
// NodeURL returns the storj.NodeURL from the Satellite system API.
|
||||||
func (system *Satellite) NodeURL() storj.NodeURL {
|
func (system *Satellite) NodeURL() storj.NodeURL {
|
||||||
return storj.NodeURL{ID: system.API.ID(), Address: system.API.Addr()}
|
return storj.NodeURL{ID: system.API.ID(), Address: system.API.Addr()}
|
||||||
|
@ -21,6 +21,7 @@ func TestSatellite_AddProject(t *testing.T) {
|
|||||||
user, err := planet.Satellites[0].AddUser(ctx, console.CreateUser{
|
user, err := planet.Satellites[0].AddUser(ctx, console.CreateUser{
|
||||||
FullName: "test user",
|
FullName: "test user",
|
||||||
Email: "test-email@test",
|
Email: "test-email@test",
|
||||||
|
Password: "password",
|
||||||
}, 4)
|
}, 4)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ type Uplink struct {
|
|||||||
|
|
||||||
APIKey map[storj.NodeID]*macaroon.APIKey
|
APIKey map[storj.NodeID]*macaroon.APIKey
|
||||||
Access map[storj.NodeID]*uplink.Access
|
Access map[storj.NodeID]*uplink.Access
|
||||||
|
User map[storj.NodeID]UserLogin
|
||||||
|
|
||||||
// Projects is indexed by the satellite number.
|
// Projects is indexed by the satellite number.
|
||||||
Projects []*Project
|
Projects []*Project
|
||||||
@ -63,6 +64,12 @@ type ProjectOwner struct {
|
|||||||
Email string
|
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.
|
// DialMetainfo dials the satellite with the appropriate api key.
|
||||||
func (project *Project) DialMetainfo(ctx context.Context) (*metaclient.Client, error) {
|
func (project *Project) DialMetainfo(ctx context.Context) (*metaclient.Client, error) {
|
||||||
return project.client.DialMetainfo(ctx, project.Satellite, project.RawAPIKey)
|
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,
|
Identity: identity,
|
||||||
APIKey: map[storj.NodeID]*macaroon.APIKey{},
|
APIKey: map[storj.NodeID]*macaroon.APIKey{},
|
||||||
Access: map[storj.NodeID]*uplink.Access{},
|
Access: map[storj.NodeID]*uplink.Access{},
|
||||||
|
User: map[storj.NodeID]UserLogin{},
|
||||||
}
|
}
|
||||||
|
|
||||||
planetUplink.Log.Debug("id=" + identity.ID.String())
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
planetUplink.User[satellite.ID()] = UserLogin{
|
||||||
|
Email: user.Email,
|
||||||
|
Password: user.FullName,
|
||||||
|
}
|
||||||
|
|
||||||
project, err := satellite.AddProject(ctx, user.ID, projectName)
|
project, err := satellite.AddProject(ctx, user.ID, projectName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
Loading…
Reference in New Issue
Block a user