Skip to content
Snippets Groups Projects
Commit f5d1b6f9 authored by Manuel Kieweg's avatar Manuel Kieweg
Browse files

Integration tests for CLI

parent 19a8499c
Branches
Tags
3 merge requests!120Resolve "Code Quality",!119Draft: Resolve "Tests for HTTP API and CLI",!90Develop
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
// Capabilities sends a gNMI Capabilities request to the specified target // Capabilities sends a gNMI Capabilities request to the specified target
// and prints the supported models to stdout // and prints the supported models to stdout
func Capabilities(a, u , p string) error { func Capabilities(a, u, p string) error {
cfg := gnmi.Config{ cfg := gnmi.Config{
Addr: a, Addr: a,
Username: u, Username: u,
......
...@@ -9,7 +9,7 @@ import ( ...@@ -9,7 +9,7 @@ import (
) )
// Get sends a gNMI Get request to the specified target and prints the response to stdout // Get sends a gNMI Get request to the specified target and prints the response to stdout
func Get(a, u, p string, args...string) error { func Get(a, u, p string, args ...string) (*gpb.GetResponse,error) {
sbi := &nucleus.OpenConfig{} sbi := &nucleus.OpenConfig{}
opts := &nucleus.GnmiTransportOptions{ opts := &nucleus.GnmiTransportOptions{
Config: gnmi.Config{ Config: gnmi.Config{
...@@ -22,12 +22,16 @@ func Get(a, u, p string, args...string) error { ...@@ -22,12 +22,16 @@ func Get(a, u, p string, args...string) error {
} }
t, err := nucleus.NewGnmiTransport(opts) t, err := nucleus.NewGnmiTransport(opts)
if err != nil { if err != nil {
return err return nil,err
} }
resp, err := t.Get(context.Background(), args...) resp, err := t.Get(context.Background(), args...)
if err != nil { if err != nil {
return err return nil, err
} }
log.Info(resp) log.Debug(resp)
return nil r, ok := resp.(*gpb.GetResponse)
if !ok {
return nil, &nucleus.ErrInvalidTypeAssertion{}
}
return r, nil
} }
package cli package cli
import ( import (
"errors"
"fmt" "fmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
...@@ -34,13 +35,14 @@ func HttpGet(apiEndpoint, f string, args ...string) error { ...@@ -34,13 +35,14 @@ func HttpGet(apiEndpoint, f string, args ...string) error {
if err != nil { if err != nil {
return err return err
} }
if f == "init" { switch f {
case "init":
pnd := string(bytes[:36]) pnd := string(bytes[:36])
sbi := string(bytes[36:]) sbi := string(bytes[36:])
viper.Set("CLI_PND", pnd) viper.Set("CLI_PND", pnd)
viper.Set("CLI_SBI", sbi) viper.Set("CLI_SBI", sbi)
return viper.WriteConfig() return viper.WriteConfig()
} else { default:
fmt.Println(string(bytes)) fmt.Println(string(bytes))
} }
case http.StatusCreated: case http.StatusCreated:
...@@ -49,11 +51,14 @@ func HttpGet(apiEndpoint, f string, args ...string) error { ...@@ -49,11 +51,14 @@ func HttpGet(apiEndpoint, f string, args ...string) error {
if err != nil { if err != nil {
return err return err
} }
uuid := string(bytes[19:55])
viper.Set("LAST_DEVICE_UUID", uuid)
fmt.Println(string(bytes)) fmt.Println(string(bytes))
default: default:
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"status code": resp.StatusCode, "status code": resp.StatusCode,
}).Error("operation unsuccessful") }).Error("operation unsuccessful")
return errors.New(resp.Status)
} }
return nil return nil
} }
...@@ -8,10 +8,10 @@ import ( ...@@ -8,10 +8,10 @@ import (
var testSchema *ytypes.Schema var testSchema *ytypes.Schema
func init(){ func init() {
var err error var err error
testSchema, err = model.Schema() testSchema, err = model.Schema()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
\ No newline at end of file
package cli package cli
import ( import (
"code.fbi.h-da.de/cocsn/gosdn/forks/google/gnmi" "os"
"context"
gpb "github.com/openconfig/gnmi/proto/gnmi"
"github.com/openconfig/ygot/ygot"
"reflect"
"testing" "testing"
) )
const unreachable = "203.0.113.10:6030"
var address = "141.100.70.171:6030"
var apiEndpoint = "http://141.100.70.171:8080"
var username = "admin"
var password = "arista"
var defaultPath = []string{"/system/config/hostname"}
func testSetupIntegration() {
a := os.Getenv("GOSDN_TEST_ENDPOINT")
if a != "" {
address = a
}
api := os.Getenv("GOSDN_TEST_API_ENDPOINT")
if api != "" {
apiEndpoint = api
}
}
func TestMain(m *testing.M) {
testSetupIntegration()
os.Exit(m.Run())
}
func TestCapabilities(t *testing.T) { func TestCapabilities(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
type args struct { type args struct {
a string a string
u string u string
...@@ -20,7 +43,24 @@ func TestCapabilities(t *testing.T) { ...@@ -20,7 +43,24 @@ func TestCapabilities(t *testing.T) {
args args args args
wantErr bool wantErr bool
}{ }{
// TODO: Add test cases. {
name: "default",
args: args{
a: address,
u: username,
p: password,
},
wantErr: false,
},
{
name: "destination unreachable",
args: args{
a: unreachable,
u: username,
p: password,
},
wantErr: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
...@@ -32,6 +72,9 @@ func TestCapabilities(t *testing.T) { ...@@ -32,6 +72,9 @@ func TestCapabilities(t *testing.T) {
} }
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
type args struct { type args struct {
a string a string
u string u string
...@@ -43,11 +86,30 @@ func TestGet(t *testing.T) { ...@@ -43,11 +86,30 @@ func TestGet(t *testing.T) {
args args args args
wantErr bool wantErr bool
}{ }{
// TODO: Add test cases. {
name: "default",
args: args{
a: address,
u: username,
p: password,
args: defaultPath,
},
wantErr: false,
},
{
name: "destination unreachable",
args: args{
a: unreachable,
u: username,
p: password,
args: defaultPath,
},
wantErr: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if err := Get(tt.args.a, tt.args.u, tt.args.p, tt.args.args...); (err != nil) != tt.wantErr { if _,err := Get(tt.args.a, tt.args.u, tt.args.p, tt.args.args...); (err != nil) != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
} }
}) })
...@@ -55,6 +117,9 @@ func TestGet(t *testing.T) { ...@@ -55,6 +117,9 @@ func TestGet(t *testing.T) {
} }
func TestHttpGet(t *testing.T) { func TestHttpGet(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
type args struct { type args struct {
apiEndpoint string apiEndpoint string
f string f string
...@@ -65,7 +130,24 @@ func TestHttpGet(t *testing.T) { ...@@ -65,7 +130,24 @@ func TestHttpGet(t *testing.T) {
args args args args
wantErr bool wantErr bool
}{ }{
// TODO: Add test cases. {
name: "default",
args: args{
apiEndpoint: apiEndpoint,
f: "init",
args: nil,
},
wantErr: false,
},
{
name: "destination unreachable",
args: args{
apiEndpoint: "http://" + unreachable,
f: "init",
args: nil,
},
wantErr: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
...@@ -76,28 +158,18 @@ func TestHttpGet(t *testing.T) { ...@@ -76,28 +158,18 @@ func TestHttpGet(t *testing.T) {
} }
} }
func TestLeafPaths(t *testing.T) {
tests := []struct {
name string
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := LeafPaths(); (err != nil) != tt.wantErr {
t.Errorf("LeafPaths() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestPathTraversal(t *testing.T) { func TestPathTraversal(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
tests := []struct { tests := []struct {
name string name string
wantErr bool wantErr bool
}{ }{
// TODO: Add test cases. {
name: "default",
wantErr: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
...@@ -109,6 +181,9 @@ func TestPathTraversal(t *testing.T) { ...@@ -109,6 +181,9 @@ func TestPathTraversal(t *testing.T) {
} }
func TestSet(t *testing.T) { func TestSet(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
type args struct { type args struct {
a string a string
u string u string
...@@ -121,7 +196,39 @@ func TestSet(t *testing.T) { ...@@ -121,7 +196,39 @@ func TestSet(t *testing.T) {
args args args args
wantErr bool wantErr bool
}{ }{
// TODO: Add test cases. {
name: "default",
args: args{
a: address,
u: username,
p: password,
typ: "update",
args: []string{"/system/config/hostname", "ceos3000"},
},
wantErr: false,
},
{
name: "destination unreachable",
args: args{
a: unreachable,
u: username,
p: password,
typ: "update",
args: []string{"/system/config/hostname", "ceos3000"},
},
wantErr: true,
},
{
name: "invalid path",
args: args{
a: address,
u: username,
p: password,
typ: "update",
args: []string{"invalid/path", "ceos3000"},
},
wantErr: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
...@@ -131,129 +238,3 @@ func TestSet(t *testing.T) { ...@@ -131,129 +238,3 @@ func TestSet(t *testing.T) {
}) })
} }
} }
func TestSubscribe(t *testing.T) {
type args struct {
a string
u string
p string
sample int64
heartbeat int64
args []string
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := Subscribe(tt.args.a, tt.args.u, tt.args.p, tt.args.sample, tt.args.heartbeat, tt.args.args...); (err != nil) != tt.wantErr {
t.Errorf("Subscribe() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestTarget(t *testing.T) {
type args struct {
bindAddr string
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := Target(tt.args.bindAddr); (err != nil) != tt.wantErr {
t.Errorf("Target() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_callback(t *testing.T) {
type args struct {
newConfig ygot.ValidatedGoStruct
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := callback(tt.args.newConfig); (err != nil) != tt.wantErr {
t.Errorf("callback() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_newServer(t *testing.T) {
type args struct {
model *gnmi.Model
config []byte
}
tests := []struct {
name string
args args
want *server
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := newServer(tt.args.model, tt.args.config)
if (err != nil) != tt.wantErr {
t.Errorf("newServer() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("newServer() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_server_Get(t *testing.T) {
type fields struct {
Server *gnmi.Server
}
type args struct {
ctx context.Context
req *gpb.GetRequest
}
tests := []struct {
name string
fields fields
args args
want *gpb.GetResponse
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &server{
Server: tt.fields.Server,
}
got, err := s.Get(tt.args.ctx, tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Get() got = %v, want %v", got, tt.want)
}
})
}
}
...@@ -12,9 +12,10 @@ import ( ...@@ -12,9 +12,10 @@ import (
"syscall" "syscall"
"time" "time"
) )
// Subscribe starts a gNMI subscriber requersting the specified paths on the target and // Subscribe starts a gNMI subscriber requersting the specified paths on the target and
// logs the response to stdout. Only 'stream' mode with 'sample' operation supported. // logs the response to stdout. Only 'stream' mode with 'sample' operation supported.
func Subscribe(a, u, p string, sample, heartbeat int64, args...string) error{ func Subscribe(a, u, p string, sample, heartbeat int64, args ...string) error {
sbi := &nucleus.OpenConfig{} sbi := &nucleus.OpenConfig{}
tOpts := &nucleus.GnmiTransportOptions{ tOpts := &nucleus.GnmiTransportOptions{
Config: gnmi.Config{ Config: gnmi.Config{
...@@ -27,7 +28,7 @@ func Subscribe(a, u, p string, sample, heartbeat int64, args...string) error{ ...@@ -27,7 +28,7 @@ func Subscribe(a, u, p string, sample, heartbeat int64, args...string) error{
RespChan: make(chan *gpb.SubscribeResponse), RespChan: make(chan *gpb.SubscribeResponse),
} }
device, err := nucleus.NewDevice(sbi,tOpts) device, err := nucleus.NewDevice(sbi, tOpts)
if err != nil { if err != nil {
return err return err
} }
......
package cli
import (
"github.com/openconfig/ygot/util"
log "github.com/sirupsen/logrus"
)
func LeafPaths() error {
for _, v := range testSchema.SchemaTree {
entry, err := util.FindLeafRefSchema(v, "/interface/")
if err != nil {
log.Error(err)
}
log.Info(entry)
}
return nil
}
...@@ -39,9 +39,10 @@ import ( ...@@ -39,9 +39,10 @@ import (
var getCmd = &cobra.Command{ var getCmd = &cobra.Command{
Use: "gosdn get", Use: "gosdn get",
Short: "get request", Short: "get request",
Long: `Sends a gNMI Get request to the specified target and prints the response to stdout`, Long: `Sends a gNMI Get request to the specified target and prints the response to stdout`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return cli.Get(address, username, password, args...) _,err := cli.Get(address, username, password, args...)
return err
}, },
} }
......
...@@ -47,7 +47,7 @@ var getDeviceCmd = &cobra.Command{ ...@@ -47,7 +47,7 @@ var getDeviceCmd = &cobra.Command{
"uuid="+uuid, "uuid="+uuid,
"sbi="+cliSbi, "sbi="+cliSbi,
"pnd="+cliPnd, "pnd="+cliPnd,
) )
}, },
} }
......
...@@ -33,5 +33,5 @@ package main ...@@ -33,5 +33,5 @@ package main
import "code.fbi.h-da.de/cocsn/gosdn/cmd" import "code.fbi.h-da.de/cocsn/gosdn/cmd"
func main() { func main() {
cmd.Execute() cmd.Execute()
} }
...@@ -41,7 +41,7 @@ var initCmd = &cobra.Command{ ...@@ -41,7 +41,7 @@ var initCmd = &cobra.Command{
Short: "initialise SBI and PND", Short: "initialise SBI and PND",
Long: ``, Long: ``,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return cli.HttpGet(apiEndpoint, "init" ) return cli.HttpGet(apiEndpoint, "init")
}, },
} }
......
package cmd
import (
"code.fbi.h-da.de/cocsn/gosdn/cli"
guuid "github.com/google/uuid"
"github.com/spf13/viper"
"os"
"testing"
)
var testAddress = "141.100.70.171:6030"
var testApiEndpoint = "http://141.100.70.171:8080"
var testUsername = "admin"
var testPassword = "arista"
func testSetupIntegration() {
a := os.Getenv("GOSDN_TEST_ENDPOINT")
if a != "" {
testAddress = a
}
api := os.Getenv("GOSDN_TEST_API_ENDPOINT")
if api != "" {
testApiEndpoint = api
}
u := os.Getenv("GOSDN_TEST_USER")
if u != "" {
testUsername = u
}
p := os.Getenv("GOSDN_TEST_PASSWORD")
if p != "" {
testPassword = p
}
}
func TestMain(m *testing.M) {
testSetupIntegration()
os.Exit(m.Run())
}
func TestCliIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
tests := []struct {
name string
wantErr bool
}{
{
name: "default",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer viper.Reset()
if err := cli.HttpGet(testApiEndpoint, "init"); (err != nil) != tt.wantErr {
switch err.(type) {
case viper.ConfigFileNotFoundError:
default:
t.Errorf("gosdn cli init error = %v, wantErr %v", err, tt.wantErr)
return
}
}
cliPnd = viper.GetString("CLI_PND")
cliSbi = viper.GetString("CLI_SBI")
if err := cli.HttpGet(
testApiEndpoint,
"addDevice",
"address="+testAddress,
"password="+testPassword,
"username="+testUsername,
"sbi="+cliSbi,
"pnd="+cliPnd,
); (err != nil) != tt.wantErr {
t.Errorf("gosdn cli add-device error = %v, wantErr %v", err, tt.wantErr)
return
}
did := viper.GetString("LAST_DEVICE_UUID")
if err := cli.HttpGet(
testApiEndpoint,
"request",
"uuid="+did,
"sbi="+cliSbi,
"pnd="+cliPnd,
"path=/system/config/hostname",
); (err != nil) != tt.wantErr {
t.Errorf("gosdn cli request error = %v, wantErr %v", err, tt.wantErr)
return
}
if err := cli.HttpGet(
testApiEndpoint,
"getDevice",
"address="+testAddress,
"uuid="+did,
"sbi="+cliSbi,
"pnd="+cliPnd,
); (err != nil) != tt.wantErr {
t.Errorf("gosdn cli get-device error = %v, wantErr %v", err, tt.wantErr)
return
}
hostname := guuid.New().String()
if err := cli.HttpGet(
testApiEndpoint,
"set",
"address="+testAddress,
"uuid="+did,
"sbi="+cliSbi,
"pnd="+cliPnd,
"path=/system/config/hostname",
"value="+hostname,
); (err != nil) != tt.wantErr {
t.Errorf("gosdn cli set error = %v, wantErr %v", err, tt.wantErr)
return
}
resp, err := cli.Get(testAddress, testUsername, testPassword, "/system/config/hostname")
if (err != nil) != tt.wantErr {
t.Errorf("cli.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
got := resp.Notification[0].Update[0].Val.GetStringVal()
if got != hostname {
t.Errorf("integration test failed = got: %v, want: %v", got, hostname)
}
})
}
}
...@@ -39,7 +39,7 @@ import ( ...@@ -39,7 +39,7 @@ import (
var legacyCmd = &cobra.Command{ var legacyCmd = &cobra.Command{
Use: "legacy", Use: "legacy",
Short: "multiple ygot utils - not yet implemented", Short: "multiple ygot utils - not yet implemented",
Long: ``, Long: ``,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("not implemented") return errors.New("not implemented")
}, },
......
...@@ -39,7 +39,7 @@ import ( ...@@ -39,7 +39,7 @@ import (
var pathCmd = &cobra.Command{ var pathCmd = &cobra.Command{
Use: "path", Use: "path",
Short: "multiple ygot utils - not yet implemented", Short: "multiple ygot utils - not yet implemented",
Long: ``, Long: ``,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("not implemented") return errors.New("not implemented")
}, },
......
...@@ -48,7 +48,7 @@ var requestCmd = &cobra.Command{ ...@@ -48,7 +48,7 @@ var requestCmd = &cobra.Command{
"sbi="+cliSbi, "sbi="+cliSbi,
"pnd="+cliPnd, "pnd="+cliPnd,
"path="+args[0], "path="+args[0],
) )
}, },
} }
......
...@@ -47,7 +47,7 @@ var requestAllCmd = &cobra.Command{ ...@@ -47,7 +47,7 @@ var requestAllCmd = &cobra.Command{
"sbi="+cliSbi, "sbi="+cliSbi,
"pnd="+cliPnd, "pnd="+cliPnd,
"path="+args[0], "path="+args[0],
) )
}, },
} }
......
...@@ -51,6 +51,6 @@ var setCmd = &cobra.Command{ ...@@ -51,6 +51,6 @@ var setCmd = &cobra.Command{
func init() { func init() {
rootCmd.AddCommand(setCmd) rootCmd.AddCommand(setCmd)
setCmd.Flags().StringVarP(&typ, "type", "t", "update", "Type of the set request. " + setCmd.Flags().StringVarP(&typ, "type", "t", "update", "Type of the set request. "+
"Possible values: 'update', 'replace', and 'delete'") "Possible values: 'update', 'replace', and 'delete'")
} }
...@@ -42,7 +42,7 @@ var bindAddr string ...@@ -42,7 +42,7 @@ var bindAddr string
var targetCmd = &cobra.Command{ var targetCmd = &cobra.Command{
Use: "target", Use: "target",
Short: "start gnmi target", Short: "start gnmi target",
Long: `Starts a gNMI target listening on the specified port.`, Long: `Starts a gNMI target listening on the specified port.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return cli.Target(bindAddr) return cli.Target(bindAddr)
}, },
......
...@@ -39,7 +39,7 @@ import ( ...@@ -39,7 +39,7 @@ import (
var utilCmd = &cobra.Command{ var utilCmd = &cobra.Command{
Use: "util", Use: "util",
Short: "multiple ygot utils - not yet implemented", Short: "multiple ygot utils - not yet implemented",
Long: ``, Long: ``,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("not implemented") return errors.New("not implemented")
}, },
......
...@@ -40,7 +40,7 @@ import ( ...@@ -40,7 +40,7 @@ import (
var ygotCmd = &cobra.Command{ var ygotCmd = &cobra.Command{
Use: "ygot", Use: "ygot",
Short: "multiple ygot utils - not yet implemented", Short: "multiple ygot utils - not yet implemented",
Long: ``, Long: ``,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("not implemented") return errors.New("not implemented")
}, },
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment