diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6dc2589 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go +{ + "name": "Go", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/go:1-1-bullseye" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d75af5e --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +SQLITE_CONNECTION_STRING=sqlitecloud://myhost.sqlite.cloud +SQLITE_USER=admin +SQLITE_PASSWORD= +SQLITE_API_KEY= +SQLITE_HOST=myhost.sqlite.cloud +SQLITE_DB=chinook.sqlite +SQLITE_PORT=8860 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.gitignore b/.gitignore index 3004869..7552c31 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ cli/.DS_Store .DS_Store cli/.vscode .vscode/launch.json +pkg/ +bin +.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3bd92f0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "gopls": { + "ui.semanticTokens": true + } +} diff --git a/cli/sqlc.go b/cli/sqlc.go index 3d30302..aa7ec7f 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -91,6 +91,15 @@ Connection Options: -w, --password PASSWORD Use PASSWORD for authentication -t, --timeout SECS Set Timeout for network operations to SECS seconds [default::10] -c, --compress (NO|LZ4) Use line compression [default::NO] + -z, --zerotext Text ends with 0 value + -m, --memory Use in-memory database + -e, --create Create database if it does not exist + -i, --nonlinearizable Use non-linearizable mode for queries + -a, --apikey KEY Use API key for authentication + -n, --noblob Disable BLOB support + -x, --maxdata SIZE Set maximum data size for queries + -y, --maxrows ROWS Set maximum number of rows for queries + -r, --maxrowset ROWS Set maximum number of rows per rowset for queries --tls [YES|NO|INTERN|FILE] Encrypt the database connection using the host's root CA set (YES), a custom CA with a PEM from FILE (FILE), the internal SQLiteCloud CA (INTERN), or disable the encryption (NO) [default::YES] ` @@ -131,16 +140,20 @@ type Parameter struct { Format string `docopt:"--format"` OutPutFormat int `docopt:"--outputformat"` - Host string `docopt:"--host"` - Port int `docopt:"--port"` - User string `docopt:"--user"` - Password string `docopt:"--password"` - Database string `docopt:"--dbname"` - ApiKey string `docopt:"--apikey"` - NoBlob bool `docopt:"--noblob"` - MaxData int `docopt:"--maxdata"` - MaxRows int `docopt:"--maxrows"` - MaxRowset int `docopt:"--maxrowset"` + Host string `docopt:"--host"` + Port int `docopt:"--port"` + User string `docopt:"--user"` + Password string `docopt:"--password"` + Database string `docopt:"--dbname"` + Zerotext bool `docopt:"--zerotext"` + Memory bool `docopt:"--memory"` + Create bool `docopt:"--create"` + NonLinearizable bool `docopt:"--nonlinearizable"` + ApiKey string `docopt:"--apikey"` + NoBlob bool `docopt:"--noblob"` + MaxData int `docopt:"--maxdata"` + MaxRows int `docopt:"--maxrows"` + MaxRowset int `docopt:"--maxrowset"` Timeout int `docopt:"--timeout"` Compress string `docopt:"--compress"` @@ -245,23 +258,46 @@ func parseParameters() (Parameter, error) { p["--user"] = getFirstNoneEmptyString([]string{dropError(p.String("--user")), conf.Username}) p["--password"] = getFirstNoneEmptyString([]string{dropError(p.String("--password")), conf.Password}) p["--dbname"] = getFirstNoneEmptyString([]string{dropError(p.String("--dbname")), conf.Database}) - p["--host"] = getFirstNoneEmptyString([]string{dropError(p.String("--host")), conf.Host}) - p["--compress"] = getFirstNoneEmptyString([]string{dropError(p.String("--compress")), conf.CompressMode}) if conf.Port > 0 { p["--port"] = getFirstNoneEmptyString([]string{dropError(p.String("--port")), fmt.Sprintf("%d", conf.Port)}) } if conf.Timeout > 0 { p["--timeout"] = getFirstNoneEmptyString([]string{dropError(p.String("--timeout")), fmt.Sprintf("%d", conf.Timeout)}) } + if conf.Compression { + p["--compress"] = getFirstNoneEmptyString([]string{dropError(p.String("--compress")), conf.CompressMode}) + } + if conf.Zerotext { + if b, err := p.Bool("--zerotext"); err == nil { + p["--zerotext"] = b || conf.Zerotext + } + } + if conf.Memory { + if b, err := p.Bool("--memory"); err == nil { + p["--memory"] = b || conf.Memory + } + } + if conf.Create { + if b, err := p.Bool("--create"); err == nil { + p["--create"] = b || conf.Create + } + } + if conf.NonLinearizable { + if b, err := p.Bool("--nonlinearizable"); err == nil { + p["--nonlinearizable"] = b || conf.NonLinearizable + } + } p["--tls"] = getFirstNoneEmptyString([]string{dropError(p.String("--tls")), conf.Pem}) - if conf.Secure == false { + if !conf.Secure { p["--tls"] = "NO" - } else if conf.TlsInsecureSkipVerify == true { + } else if conf.TlsInsecureSkipVerify { p["--tls"] = "SKIP" } p["--apikey"] = getFirstNoneEmptyString([]string{dropError(p.String("--apikey")), conf.ApiKey}) if conf.NoBlob { - p["--noblob"] = getFirstNoneEmptyString([]string{dropError(p.String("--noblob")), strconv.FormatBool(conf.NoBlob)}) + if b, err := p.Bool("--noblob"); err == nil { + p["--noblob"] = b || conf.NoBlob + } } if conf.MaxData > 0 { p["--maxdata"] = getFirstNoneEmptyString([]string{dropError(p.String("--maxdata")), fmt.Sprintf("%d", conf.MaxData)}) @@ -284,6 +320,9 @@ func parseParameters() (Parameter, error) { p["--compress"] = getFirstNoneEmptyString([]string{dropError(p.String("--compress")), "NO"}) p["--tls"] = getFirstNoneEmptyString([]string{dropError(p.String("--tls")), "YES"}) p["--separator"] = getFirstNoneEmptyString([]string{dropError(p.String("--separator")), dropError(sqlitecloud.GetDefaultSeparatorForOutputFormat(outputformat)), "|"}) + p["--maxdata"] = getFirstNoneEmptyString([]string{dropError(p.String("--maxdata")), "0"}) + p["--maxrows"] = getFirstNoneEmptyString([]string{dropError(p.String("--maxrows")), "0"}) + p["--maxrowset"] = getFirstNoneEmptyString([]string{dropError(p.String("--maxrowset")), "0"}) // Fix invalid(=unset) parameters, quotation & control-chars for k, v := range p { @@ -375,18 +414,22 @@ func main() { // print( out, fmt.Sprintf( "%s %s, %s", long_name, version, copyright ), ¶meter ) config := sqlitecloud.SQCloudConfig{ - Host: parameter.Host, - Port: parameter.Port, - Username: parameter.User, - Password: parameter.Password, - Database: parameter.Database, - Timeout: time.Duration(parameter.Timeout) * time.Second, - CompressMode: parameter.Compress, - ApiKey: parameter.ApiKey, - NoBlob: parameter.NoBlob, - MaxData: parameter.MaxData, - MaxRows: parameter.MaxRows, - MaxRowset: parameter.MaxRowset, + Host: parameter.Host, + Port: parameter.Port, + Username: parameter.User, + Password: parameter.Password, + Database: parameter.Database, + Timeout: time.Duration(parameter.Timeout) * time.Second, + CompressMode: parameter.Compress, + Zerotext: parameter.Zerotext, + Memory: parameter.Memory, + Create: parameter.Create, + NonLinearizable: parameter.NonLinearizable, + ApiKey: parameter.ApiKey, + NoBlob: parameter.NoBlob, + MaxData: parameter.MaxData, + MaxRows: parameter.MaxRows, + MaxRowset: parameter.MaxRowset, } config.Secure, config.TlsInsecureSkipVerify, config.Pem = sqlitecloud.ParseTlsString(parameter.Tls) @@ -514,7 +557,7 @@ func main() { case ".timeout": parameter.Timeout = getNextTokenValueAsInteger(out, parameter.Timeout, 10, tokens, ¶meter) case ".compress": - parameter.Compress = getNextTokenValueAsString(out, parameter.Compress, "NO", "|no|lz4|", tokens, ¶meter) + parameter.Compress = getNextTokenValueAsString(out, parameter.Compress, sqlitecloud.CompressModeNo, "|no|lz4|", tokens, ¶meter) db.Compress(parameter.Compress) case ".format": diff --git a/connection.go b/connection.go index 599c5a1..6c7db29 100644 --- a/connection.go +++ b/connection.go @@ -37,10 +37,16 @@ type SQCloudConfig struct { Username string Password string Database string - Timeout time.Duration - CompressMode string - Secure bool - TlsInsecureSkipVerify bool + PasswordHashed bool // Password is hashed + Timeout time.Duration // Optional query timeout passed directly to TLS socket + CompressMode string // eg: LZ4 + Compression bool // Enable compression + Zerotext bool // Tell the server to zero-terminate strings + Memory bool // Database will be created in memory + Create bool // Create the database if it doesn't exist? + Secure bool // Connect using plain TCP port, without TLS encryption, NOT RECOMMENDED (insecure) + NonLinearizable bool // Request for immediate responses from the server node without waiting for linerizability guarantees + TlsInsecureSkipVerify bool // Accept invalid TLS certificates (no_verify_certificate) Pem string ApiKey string NoBlob bool // flag to tell the server to not send BLOB columns @@ -69,6 +75,9 @@ type SQCloud struct { ErrorMessage string } +const CompressModeNo = "NO" +const CompressModeLZ4 = "LZ4" + const SQLiteCloudCA = "SQLiteCloudCA" func New(config SQCloudConfig) *SQCloud { @@ -104,15 +113,20 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err config.Password, _ = u.User.Password() config.Database = strings.TrimPrefix(u.Path, "/") config.Timeout = 0 - config.CompressMode = "NO" + config.Compression = false + config.CompressMode = CompressModeNo + config.Zerotext = false + config.Memory = false + config.Create = false config.Secure = true + config.NonLinearizable = false config.TlsInsecureSkipVerify = false config.Pem = "" - config.ApiKey = "" config.NoBlob = false config.MaxData = 0 config.MaxRows = 0 config.MaxRowset = 0 + config.ApiKey = "" sPort := strings.TrimSpace(u.Port()) if len(sPort) > 0 { @@ -133,6 +147,42 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err case "compress": config.CompressMode = strings.ToUpper(lastLiteral) + if config.CompressMode == CompressModeLZ4 { + config.Compression = true + } + case "compression": + if b, err := parseBool(lastLiteral, config.Compression); err == nil && b { + config.Compression = true + config.CompressMode = CompressModeLZ4 + } + case "zerotext": + if b, err := parseBool(lastLiteral, config.Zerotext); err == nil { + config.Zerotext = b + } + case "memory": + if b, err := parseBool(lastLiteral, config.Memory); err == nil { + config.Memory = b + } + case "create": + if b, err := parseBool(lastLiteral, config.Create); err == nil { + config.Create = b + } + case "secure": + if b, err := parseBool(lastLiteral, config.Secure); err == nil { + config.Secure = b + } + case "insecure": + if b, err := parseBool(lastLiteral, config.Secure); err == nil { + config.Secure = !b + } + case "non_linearizable", "nonlinearizable": + if b, err := parseBool(lastLiteral, config.NonLinearizable); err == nil { + config.NonLinearizable = b + } + case "no_verify_certificate": + if b, err := parseBool(lastLiteral, config.TlsInsecureSkipVerify); err == nil { + config.TlsInsecureSkipVerify = b + } case "tls": config.Secure, config.TlsInsecureSkipVerify, config.Pem = ParseTlsString(lastLiteral) case "apikey": @@ -210,8 +260,8 @@ func (this *SQCloud) CheckConnectionParameter() error { return errors.New(fmt.Sprintf("Invalid Timeout (%s)", this.Timeout.String())) } - switch strings.ToUpper(this.CompressMode) { - case "NO", "LZ4": + switch this.CompressMode { + case CompressModeNo, CompressModeLZ4: default: return errors.New(fmt.Sprintf("Invalid compression method (%s)", this.CompressMode)) } @@ -340,45 +390,7 @@ func (this *SQCloud) reconnect() error { } } - commands := "" - args := []interface{}{} - - if strings.TrimSpace(this.Username) != "" { - c, a := authCommand(this.Username, this.Password) - commands += c - args = append(args, a...) - - } else if strings.TrimSpace(this.ApiKey) != "" { - c, a := authWithKeyCommand(this.ApiKey) - commands += c - args = append(args, a...) - } - - if strings.TrimSpace(this.Database) != "" { - c, a := useDatabaseCommand(this.Database) - commands += c - args = append(args, a...) - } - - if this.NoBlob { - commands += noblobCommand(this.NoBlob) - } - - if this.MaxData > 0 { - commands += maxdataCommand(this.MaxData) - } - - if this.MaxRows > 0 { - commands += maxrowsCommand(this.MaxRows) - } - - if this.MaxRowset > 0 { - commands += maxrowsetCommand(this.MaxRowset) - } - - if this.CompressMode != "NO" { - commands += compressCommand(this.CompressMode) - } + commands, args := connectionCommands(this.SQCloudConfig) if commands != "" { if len(args) > 0 { @@ -421,6 +433,53 @@ func (this *SQCloud) Close() error { return nil } +func connectionCommands(config SQCloudConfig) (string, []interface{}) { + buffer := "" + args := []interface{}{} + + // it must be executed before authentication command + if config.NonLinearizable { + buffer += nonlinearizableCommand(config.NonLinearizable) + } + + if config.ApiKey != "" { + c, a := authWithKeyCommand(config.ApiKey) + buffer += c + args = append(args, a...) + } + + if config.Username != "" && config.Password != "" { + c, a := authCommand(config.Username, config.Password, config.PasswordHashed) + buffer += c + args = append(args, a...) + } + + if config.Database != "" { + create := config.Create && !config.Memory + c, a := useDatabaseCommand(config.Database, create) + buffer += c + args = append(args, a...) + } + + buffer += compressCommand(config.CompressMode) + + if config.Zerotext { + buffer += zerotextCommand(config.Zerotext) + } + + if config.NoBlob { + buffer += noblobCommand(config.NoBlob) + } + + buffer += maxdataCommand(config.MaxData) + + buffer += maxrowsCommand(config.MaxRows) + + buffer += maxrowsetCommand(config.MaxRowset) + + return buffer, args +} + func noblobCommand(NoBlob bool) string { if NoBlob { return "SET CLIENT KEY NOBLOB TO 1;" @@ -443,15 +502,31 @@ func maxrowsetCommand(v int) string { func compressCommand(CompressMode string) string { switch compression := strings.ToUpper(CompressMode); { - case compression == "NO": + case compression == CompressModeNo: return "SET CLIENT KEY COMPRESSION TO 0;" - case compression == "LZ4": + case compression == CompressModeLZ4: return "SET CLIENT KEY COMPRESSION TO 1;" default: return "" } } +func nonlinearizableCommand(NonLinearizable bool) string { + if NonLinearizable { + return "SET CLIENT KEY NONLINEARIZABLE TO 1;" + } else { + return "SET CLIENT KEY NONLINEARIZABLE TO 0;" + } +} + +func zerotextCommand(Zerotext bool) string { + if Zerotext { + return "SET CLIENT KEY ZEROTEXT TO 1;" + } else { + return "SET CLIENT KEY ZEROTEXT TO 0;" + } +} + // Compress enabled or disables data compression for this connection. // If enabled, the data is compressed with the LZ4 compression algorithm, otherwise no compression is applied the data. func (this *SQCloud) Compress(CompressMode string) error { diff --git a/server.go b/server.go index 1813142..5b48491 100644 --- a/server.go +++ b/server.go @@ -246,13 +246,18 @@ func (this *SQCloud) ListDatabaseConnections(Database string) ([]SQCloudConnecti // Auth Functions -func authCommand(Username string, Password string) (string, []interface{}) { - return "AUTH USER ? PASSWORD ?;", []interface{}{Username, Password} +func authCommand(Username string, Password string, IsPasswordHashed bool) (string, []interface{}) { + command := "PASSWORD" + if IsPasswordHashed { + command = "HASH" + } + + return fmt.Sprintf("AUTH USER ? %s ?;", command), []interface{}{Username, Password} } // Auth - INTERNAL SERVER COMMAND: Authenticates User with the given credentials. func (this *SQCloud) Auth(Username string, Password string) error { - return this.ExecuteArray(authCommand(Username, Password)) + return this.ExecuteArray(authCommand(Username, Password, false)) } func authWithKeyCommand(Key string) (string, []interface{}) { @@ -318,8 +323,12 @@ func (this *SQCloud) GetDatabase() (string, error) { return "", err } -func useDatabaseCommand(Database string) (string, []interface{}) { - return "USE DATABASE ?;", []interface{}{Database} +func useDatabaseCommand(Database string, create bool) (string, []interface{}) { + command := "USE DATABASE ?;" + if create { + command = "CREATE DATABASE ? IF NOT EXISTS;" + } + return command, []interface{}{Database} } // UseDatabase - INTERNAL SERVER COMMAND: Selects the specified Database for usage. @@ -327,7 +336,7 @@ func useDatabaseCommand(Database string) (string, []interface{}) { // An error is returned if the specified Database was not found or the user has not the necessary access rights to work with this Database. func (this *SQCloud) UseDatabase(Database string) error { this.Database = Database - return this.ExecuteArray(useDatabaseCommand(Database)) + return this.ExecuteArray(useDatabaseCommand(Database, false)) } // UseDatabase - INTERNAL SERVER COMMAND: Releases the actual Database. diff --git a/test/commons_test.go b/test/commons_test.go new file mode 100644 index 0000000..ca4fd5b --- /dev/null +++ b/test/commons_test.go @@ -0,0 +1,27 @@ +package test + +import ( + "flag" + "log" + "os" + + "github.com/joho/godotenv" +) + +func init() { + if err := godotenv.Load("./../.env"); err != nil { + log.Print("No .env file found") + } + + testConnectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + flag.StringVar(&testConnectionString, "server", testConnectionString, "Connection String") +} + +func contains[T comparable](s []T, e T) bool { + for _, v := range s { + if v == e { + return true + } + } + return false +} diff --git a/test/compress_test.go b/test/compress_test.go index 01955a0..44922f4 100644 --- a/test/compress_test.go +++ b/test/compress_test.go @@ -21,6 +21,7 @@ import ( "fmt" "math/rand" "net/url" + "os" "testing" sqlitecloud "github.com/sqlitecloud/go-sdk" @@ -31,7 +32,11 @@ const testCompressKey = "compress" const testCompressValue = "LZ4" func TestCompress(t *testing.T) { - url, err := url.Parse(testConnectionString) + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "?apikey=" + apikey + + url, err := url.Parse(connectionString) values := url.Query() values.Add(testCompressKey, testCompressValue) url.RawQuery = values.Encode() diff --git a/test/go.mod b/test/go.mod index 071503e..afb7862 100644 --- a/test/go.mod +++ b/test/go.mod @@ -2,15 +2,22 @@ module github.com/sqlitecloud/go-sdk/test go 1.18 -require github.com/sqlitecloud/go-sdk v0.0.0 +require ( + github.com/joho/godotenv v1.5.1 + github.com/sqlitecloud/go-sdk v0.0.0 + github.com/stretchr/testify v1.9.0 +) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xo/dburl v0.13.1 // indirect golang.org/x/sys v0.7.0 // indirect golang.org/x/term v0.6.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/sqlitecloud/go-sdk v0.0.0 => ../ diff --git a/test/go.sum b/test/go.sum index 9d229bd..12d63d6 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,7 +1,11 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -9,11 +13,19 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/dburl v0.13.1 h1:EV+BCdo539sc/mBrny0VxaEGLM0b1U0mJA9RpP80ux0= github.com/xo/dburl v0.13.1/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/literals_test.go b/test/literals_test.go index bbd4671..2ac679a 100644 --- a/test/literals_test.go +++ b/test/literals_test.go @@ -18,6 +18,7 @@ package test import ( + "os" "testing" sqlitecloud "github.com/sqlitecloud/go-sdk" @@ -31,7 +32,9 @@ func TestLiterals(t *testing.T) { var err error // start := time.Now() - if db, err = sqlitecloud.Connect(testConnectionString); err != nil { + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + if db, err = sqlitecloud.Connect(connectionString + "?apikey=" + apikey); err != nil { t.Fatal("CONNECT: ", err.Error()) } defer db.Close() diff --git a/test/pubsub_test.go b/test/pubsub_test.go index 21df12b..32fb425 100644 --- a/test/pubsub_test.go +++ b/test/pubsub_test.go @@ -20,6 +20,7 @@ package test import ( "encoding/json" "fmt" + "os" "reflect" "testing" "time" @@ -32,13 +33,17 @@ const testPubsubChannelName = "TestPubsubChannel" var testPubsubMessage = map[string]string{"msg_id": "12345", "msg_content": "this is the content"} func TestPubsub(t *testing.T) { - db1, err := sqlitecloud.Connect(testConnectionString) + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "?apikey=" + apikey + + db1, err := sqlitecloud.Connect(connectionString) if err != nil { t.Fatal("Connect 1: ", err.Error()) } defer db1.Close() - db2, err := sqlitecloud.Connect(testConnectionString) + db2, err := sqlitecloud.Connect(connectionString) if err != nil { t.Fatal("Connect 2: ", err.Error()) } diff --git a/test/selectarray_test.go b/test/selectarray_test.go index 144f6b5..4a2eaf4 100644 --- a/test/selectarray_test.go +++ b/test/selectarray_test.go @@ -18,6 +18,7 @@ package test import ( + "os" "testing" sqlitecloud "github.com/sqlitecloud/go-sdk" @@ -27,8 +28,11 @@ const testDbnameSelectArray = "test-gosdk-selectarray-db.sqlite" func TestSelectArray(t *testing.T) { // Server API test + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "?apikey=" + apikey - config, err1 := sqlitecloud.ParseConnectionString(testConnectionString) + config, err1 := sqlitecloud.ParseConnectionString(connectionString) if err1 != nil { t.Fatal(err1.Error()) } @@ -104,8 +108,11 @@ func TestSelectArray(t *testing.T) { func TestSelectArrayTableNameWithQuotes(t *testing.T) { // Server API test + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "?apikey=" + apikey - config, err1 := sqlitecloud.ParseConnectionString(testConnectionString) + config, err1 := sqlitecloud.ParseConnectionString(connectionString) if err1 != nil { t.Fatal(err1.Error()) } diff --git a/test/server_test.go b/test/server_test.go index f3994de..353aeb7 100644 --- a/test/server_test.go +++ b/test/server_test.go @@ -19,6 +19,7 @@ package test import ( "fmt" + "os" "strings" "testing" @@ -28,19 +29,24 @@ import ( const testDbnameServer = "test-gosdk-server-db.sqlite" func TestServer(t *testing.T) { - db, err := sqlitecloud.Connect(testConnectionString) + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + db, err := sqlitecloud.Connect(connectionString + "?apikey=" + apikey) if err != nil { t.Fatal("Connect: ", err.Error()) } // Checking wrong AUTH - if err := db.Auth(testUsername, "wrong password"); err == nil { + username, _ := os.LookupEnv("SQLITE_USER") + password, _ := os.LookupEnv("SQLITE_PASSWORD") + + if err := db.Auth(username, "wrong password"); err == nil { t.Fatal("AUTH: Expected authorization failed, got authorized") } db.Close() // reopen the connection (it was closed because of the auth command with wrong credentials) - db, err = sqlitecloud.Connect(testConnectionString) + db, err = sqlitecloud.Connect(connectionString + "?apikey=" + apikey) if err != nil { t.Fatal(err.Error()) } @@ -52,7 +58,7 @@ func TestServer(t *testing.T) { } // Checking AUTH - if err := db.Auth(testUsername, testPassword); err != nil { + if err := db.Auth(username, password); err != nil { t.Fatal("Checking AUTH: ", err.Error()) } diff --git a/test/test_commons.go b/test/test_commons.go deleted file mode 100644 index a781287..0000000 --- a/test/test_commons.go +++ /dev/null @@ -1,23 +0,0 @@ -package test - -import "flag" - -const testConnectionStringLocalhost = "sqlitecloud://admin:admin@localhost:8860?tls=skip" - -const testUsername = "admin" -const testPassword = "admin" - -var testConnectionString string = testConnectionStringLocalhost - -func init() { - flag.StringVar(&testConnectionString, "server", testConnectionStringLocalhost, "Connection String") -} - -func contains[T comparable](s []T, e T) bool { - for _, v := range s { - if v == e { - return true - } - } - return false -} diff --git a/test/unit/connection_test.go b/test/unit/connection_test.go new file mode 100644 index 0000000..4d795de --- /dev/null +++ b/test/unit/connection_test.go @@ -0,0 +1,329 @@ +package test + +import ( + "fmt" + "reflect" + "testing" + "time" + + sqlitecloud "github.com/sqlitecloud/go-sdk" + "github.com/stretchr/testify/assert" +) + +func TestParseConnectionString(t *testing.T) { + connectionString := "sqlitecloud://myhost.sqlite.cloud:8860/mydatabase?timeout=11&compress=yes" + + expectedConfig := &sqlitecloud.SQCloudConfig{ + Host: "myhost.sqlite.cloud", + Port: 8860, + Username: "", + Password: "", + Database: "mydatabase", + Timeout: time.Duration(11) * time.Second, + CompressMode: "YES", + Secure: true, + TlsInsecureSkipVerify: false, + Pem: "", + ApiKey: "", + NoBlob: false, + MaxData: 0, + MaxRows: 0, + MaxRowset: 0, + } + + config, err := sqlitecloud.ParseConnectionString(connectionString) + if err != nil { + t.Fatalf("Failed to parse connection string: %v", err) + } + + if !reflect.DeepEqual(config, expectedConfig) { + t.Fatalf("Parsed config does not match expected config.\nExpected: %+v\nGot: %+v", expectedConfig, config) + } +} + +func TestParseConnectionStringWithAPIKey(t *testing.T) { + connectionString := "sqlitecloud://user:pass@host.com:8860/dbname?apikey=abc123&timeout=11&compress=true" + expectedConfig := &sqlitecloud.SQCloudConfig{ + Host: "host.com", + Port: 8860, + Username: "user", + Password: "pass", + Database: "dbname", + Timeout: time.Duration(11) * time.Second, + CompressMode: "TRUE", + Secure: true, + TlsInsecureSkipVerify: false, + Pem: "", + ApiKey: "abc123", + NoBlob: false, + MaxData: 0, + MaxRows: 0, + MaxRowset: 0, + } + + config, err := sqlitecloud.ParseConnectionString(connectionString) + assert.NoError(t, err) + assert.Truef(t, reflect.DeepEqual(expectedConfig, config), "Expected: %+v\nGot: %+v", expectedConfig, config) +} + +func TestParseConnectionStringWithCredentials(t *testing.T) { + connectionString := "sqlitecloud://user:pass@host.com:8860" + config, err := sqlitecloud.ParseConnectionString(connectionString) + + assert.NoError(t, err) + assert.Equal(t, "user", config.Username) + assert.Equal(t, "pass", config.Password) + assert.Equal(t, "host.com", config.Host) + assert.Equal(t, 8860, config.Port) + assert.Empty(t, config.Database) +} + +func TestParseConnectionStringWithoutCredentials(t *testing.T) { + connectionString := "sqlitecloud://host.com" + config, err := sqlitecloud.ParseConnectionString(connectionString) + + assert.NoError(t, err) + assert.Empty(t, config.Username) + assert.Empty(t, config.Password) + assert.Equal(t, "host.com", config.Host) +} + +func TestParseConnectionStringWithParameters(t *testing.T) { + tests := []struct { + param string + configParam string + value string + expectedValue any + }{ + { + param: "timeout", + configParam: "Timeout", + value: "11", + expectedValue: time.Duration(11) * time.Second, + }, + { + param: "compression", + configParam: "Compression", + value: "true", + expectedValue: true, + }, + { + param: "zerotext", + configParam: "Zerotext", + value: "true", + expectedValue: true, + }, + { + param: "memory", + configParam: "Memory", + value: "true", + expectedValue: true, + }, + { + param: "create", + configParam: "Create", + value: "true", + expectedValue: true, + }, + { + param: "insecure", + configParam: "Secure", + value: "true", + expectedValue: false, + }, + { + param: "secure", + configParam: "Secure", + value: "false", + expectedValue: false, + }, + { + param: "non_linearizable", + configParam: "NonLinearizable", + value: "true", + expectedValue: true, + }, + { + param: "nonlinearizable", + configParam: "NonLinearizable", + value: "true", + expectedValue: true, + }, + { + param: "no_verify_certificate", + configParam: "TlsInsecureSkipVerify", + value: "true", + expectedValue: true, + }, + { + param: "noblob", + configParam: "NoBlob", + value: "true", + expectedValue: true, + }, + { + param: "maxdata", + configParam: "MaxData", + value: "10", + expectedValue: 10, + }, + { + param: "maxrows", + configParam: "MaxRows", + value: "11", + expectedValue: 11, + }, + { + param: "maxrowset", + configParam: "MaxRowset", + value: "12", + expectedValue: 12, + }, + { + param: "apikey", + configParam: "ApiKey", + value: "abc123", + expectedValue: "abc123", + }, + } + + for _, tt := range tests { + t.Run(tt.param, func(t *testing.T) { + config, err := sqlitecloud.ParseConnectionString("sqlitecloud://myhost.sqlite.cloud/mydatabase?" + tt.param + "=" + tt.value) + + assert.NoError(t, err) + + actualValue := reflect.ValueOf(*config).FieldByName(tt.configParam) + if !actualValue.IsValid() { + t.Fatalf("Field %s not found in config", tt.configParam) + } else { + assert.Equal(t, tt.expectedValue, actualValue.Interface()) + } + }) + } +} + +func TestParseConnectionStringWithTLSParameter(t *testing.T) { + tests := []struct { + tlsValue string + expectedSecure bool + expectedInsecure bool + expectedPem string + }{ + { + tlsValue: "true", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "false", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "y", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "n", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "yes", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "no", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "true", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "on", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "off", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "enable", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "disable", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "enabled", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "disabled", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "skip", + expectedSecure: true, + expectedInsecure: true, + expectedPem: "", + }, + { + tlsValue: "intern", + expectedSecure: true, + expectedInsecure: false, + expectedPem: sqlitecloud.SQLiteCloudCA, + }, + { + tlsValue: "", + expectedSecure: true, + expectedInsecure: false, + expectedPem: sqlitecloud.SQLiteCloudCA, + }, + { + tlsValue: "anything-default", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "anything-default", + }, + } + + for _, tt := range tests { + t.Run(tt.tlsValue, func(t *testing.T) { + connectionString := fmt.Sprintf("sqlitecloud://myhost.sqlite.cloud:8860/mydatabase?tls=%s", tt.tlsValue) + + config, err := sqlitecloud.ParseConnectionString(connectionString) + assert.NoError(t, err) + + assert.Equal(t, tt.expectedSecure, config.Secure) + assert.Equal(t, tt.expectedInsecure, config.TlsInsecureSkipVerify) + assert.Equal(t, tt.expectedPem, config.Pem) + }) + } +}