diff --git a/auth.go b/auth.go index ea40f69..04d3bcf 100644 --- a/auth.go +++ b/auth.go @@ -3,15 +3,15 @@ package fireboltgosdk import ( "encoding/json" "fmt" - "log" ) // AuthenticationResponse definition of the authentication response type AuthenticationResponse struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - TokenType string `json:"token_type"` - Scope string `json:"scope"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` } // Authenticate sends an authentication request, and returns a newly constructed client object @@ -22,15 +22,13 @@ func Authenticate(username, password string) (*Client, error) { resp, err := request("", "POST", HostNameURL+LoginUrl, nil, string(jsonData)) if err != nil { - log.Fatal(err) return nil, fmt.Errorf("authentication request failed: %v", err) } var authResp AuthenticationResponse - err = json.Unmarshal(resp, &authResp) + err = jsonStrictUnmarshall(resp, &authResp) if err != nil { - log.Fatal(err) - return nil, fmt.Errorf("failed to unmarhal authenication response: %v", err) + return nil, fmt.Errorf("failed to unmarhal authenication response: %s", resp) } return &Client{AccessToken: authResp.AccessToken}, nil diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 0000000..4481f1a --- /dev/null +++ b/auth_test.go @@ -0,0 +1,32 @@ +package fireboltgosdk + +import ( + "testing" +) + +// TestAuthHappyPath tests normal authentication, and that the access token is actually set +func TestAuthHappyPath(t *testing.T) { + markIntegrationTest(t) + + if len(clientMock.AccessToken) == 0 { + t.Errorf("Token is not set properly") + } +} + +// TestAuthWrongCredential checks that authentication with wrong credentials returns an error +func TestAuthWrongCredential(t *testing.T) { + markIntegrationTest(t) + + if _, err := Authenticate(usernameMock, "wrong_password"); err == nil { + t.Errorf("Authentication with wrong credentials didn't return an error") + } +} + +// TestAuthEmptyCredential checks that authentication with empty password returns an error +func TestAuthEmptyCredential(t *testing.T) { + markIntegrationTest(t) + + if _, err := Authenticate(usernameMock, ""); err == nil { + t.Errorf("Authentication with empty password didn't return an error") + } +} diff --git a/client.go b/client.go index bab1554..11bb2e3 100644 --- a/client.go +++ b/client.go @@ -1,6 +1,7 @@ package fireboltgosdk import ( + "bytes" "encoding/json" "errors" "fmt" @@ -214,3 +215,11 @@ func request(accessToken string, method string, url string, params map[string]st return body, nil } + +// jsonStrictUnmarshall unmarshalls json into object, and returns an error +// if some fields are missing, or extra fields are present +func jsonStrictUnmarshall(data []byte, v any) error { + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + return decoder.Decode(v) +} diff --git a/client_test.go b/client_test.go index 6db5b7a..9ab145c 100644 --- a/client_test.go +++ b/client_test.go @@ -4,30 +4,11 @@ import ( "testing" ) -func TestAuthHappyPath(t *testing.T) { - if testing.Short() { - t.Skip() - } - config, _ := ParseDSNString(dsn) - client, err := Authenticate(config.username, config.password) - if err != nil { - t.Errorf("Authentication failed with: %s", err) - } - if len(client.AccessToken) == 0 { - t.Errorf("Token is not set properly") - } -} - +// TestGetAccountId test getting account ID with existing and not existing accounts func TestGetAccountId(t *testing.T) { - if testing.Short() { - t.Skip() - } - config, _ := ParseDSNString(dsn) - client, err := Authenticate(config.username, config.password) - if err != nil { - t.Errorf("Authentication failed with: %s", err) - } - accountId, err := client.GetAccountIdByName("firebolt") + markIntegrationTest(t) + + accountId, err := clientMock.GetAccountIdByName("firebolt") if err != nil { t.Errorf("GetAccountIdByName failed with: %s", err) } @@ -35,28 +16,22 @@ func TestGetAccountId(t *testing.T) { t.Errorf("returned empty accountId") } - _, err = client.GetAccountIdByName("firebolt_not_existing_account") + _, err = clientMock.GetAccountIdByName("firebolt_not_existing_account") if err == nil { t.Errorf("GetAccountIdByName didn't failed with not-existing account") } } -func TestGetEngineUrlByName(t *testing.T) { - if testing.Short() { - t.Skip() - } - config, _ := ParseDSNString(dsn) - client, err := Authenticate(config.username, config.password) - if err != nil { - t.Errorf("Authentication failed with: %s", err) - } +// TestGetEnginePropsByName test getting engine url by name step by step +func TestGetEnginePropsByName(t *testing.T) { + markIntegrationTest(t) - accountId, err := client.GetAccountIdByName("firebolt") + accountId, err := clientMock.GetAccountIdByName("firebolt") if err != nil { t.Errorf("GetAccountIdByName failed with: %s", err) } - engineId, err := client.GetEngineIdByName(config.engineName, accountId) + engineId, err := clientMock.GetEngineIdByName(engineNameMock, accountId) if err != nil { t.Errorf("GetEngineIdByName failed with: %s", err) } @@ -64,7 +39,7 @@ func TestGetEngineUrlByName(t *testing.T) { t.Errorf("GetEngineIdByName succeed but returned a zero length account id") } - engineUrl, err := client.GetEngineUrlById(engineId, accountId) + engineUrl, err := clientMock.GetEngineUrlById(engineId, accountId) if err != nil { t.Errorf("GetEngineUrlById failed with: %s", err) } @@ -73,21 +48,54 @@ func TestGetEngineUrlByName(t *testing.T) { } } -func TestGetEngineUrlByDatabase(t *testing.T) { - if testing.Short() { - t.Skip() - } - config, _ := ParseDSNString(dsn) - client, err := Authenticate(config.username, config.password) +// TestGetEngineUrlByName test GetEngineUrlByName function and its failure scenarios +func TestGetEngineUrlByName(t *testing.T) { + markIntegrationTest(t) + + engineUrl, err := clientMock.GetEngineUrlByName(engineNameMock, accountNameMock) if err != nil { - t.Errorf("Authentication failed with: %s", err) + t.Errorf("GetEngineUrlByName returned an error: %v", err) + } + if makeCanonicalUrl(engineUrl) != makeCanonicalUrl(engineUrlMock) { + t.Errorf("Returned engine url is not equal to a mocked engine url %s != %s", engineUrl, engineUrlMock) + } + if _, err = clientMock.GetEngineUrlByName("not_existing_engine", accountNameMock); err == nil { + t.Errorf("GetEngineUrlByName didn't return an error with not existing engine") } + if _, err = clientMock.GetEngineUrlByName(engineNameMock, "not_existing_account"); err == nil { + t.Errorf("GetEngineUrlByName didn't return an error with not existing account") + } +} - engineUrl, err := client.GetEngineUrlByDatabase(config.database, config.accountName) +// TestGetEngineUrlByDatabase checks, that the url of the default engine returns properly +func TestGetEngineUrlByDatabase(t *testing.T) { + markIntegrationTest(t) + + engineUrl, err := clientMock.GetEngineUrlByDatabase(databaseMock, accountNameMock) if err != nil { - t.Errorf("GetEngineUrlByDatabase failed with: %v, %s", err, config.accountName) + t.Errorf("GetEngineUrlByDatabase failed with: %v, %s", err, accountNameMock) } - if len(engineUrl) == 0 { - t.Errorf("GetEngineUrlById succeed but returned a zero length account id") + if makeCanonicalUrl(engineUrl) != makeCanonicalUrl(engineUrlMock) { + t.Errorf("Returned engine url is not equal to a mocked engine url %s != %s", engineUrl, engineUrlMock) + } + + if _, err = clientMock.GetEngineUrlByDatabase("not_existing_database", accountNameMock); err == nil { + t.Errorf("GetEngineUrlByDatabase didn't return an error with not existing database") + } + if _, err = clientMock.GetEngineUrlByDatabase(databaseMock, "not_existing_account"); err == nil { + t.Errorf("GetEngineUrlByDatabase didn't return an error with not existing account") + } +} + +// TestQuery tests simple query +func TestQuery(t *testing.T) { + markIntegrationTest(t) + + var queryResponse QueryResponse + if err := clientMock.Query(engineUrlMock, databaseMock, "SELECT 1", &queryResponse); err != nil { + t.Errorf("Query returned an error: %v", err) + } + if queryResponse.Rows != 1 { + t.Errorf("Query response has an invalid number of rows %d != %d", queryResponse.Rows, 1) } } diff --git a/connection_test.go b/connection_test.go new file mode 100644 index 0000000..4e8291d --- /dev/null +++ b/connection_test.go @@ -0,0 +1,30 @@ +package fireboltgosdk + +import "testing" + +// TestConnectionPrepareStatement, tests that prepare statement doesn't result into an error +func TestConnectionPrepareStatement(t *testing.T) { + emptyClient := Client{} + fireboltConnection := fireboltConnection{&emptyClient, "database_name", "engine_url"} + + queryMock := "SELECT 1" + _, err := fireboltConnection.Prepare(queryMock) + if err != nil { + t.Errorf("Prepare failed, but it shouldn't: %v", err) + } +} + +// TestConnectionClose, tests that connection close doesn't result an error +// and prepare statement on closed connection is not possible +func TestConnectionClose(t *testing.T) { + emptyClient := Client{} + fireboltConnection := fireboltConnection{&emptyClient, databaseMock, engineUrlMock} + if err := fireboltConnection.Close(); err != nil { + t.Errorf("Close failed with an err: %v", err) + } + + _, err := fireboltConnection.Prepare("SELECT 1") + if err == nil { + t.Errorf("Prepare on closed connection didn't fail, but it should") + } +} diff --git a/driver_test.go b/driver_test.go index e356658..15a4f9a 100644 --- a/driver_test.go +++ b/driver_test.go @@ -5,34 +5,48 @@ import ( "database/sql" "fmt" "os" + "reflect" "testing" + "time" ) var ( - dsn string - dsnDefaultEngine string - dsnDefaultAccount string - username string - password string - database string - engineUrl string - engineName string - accountName string + dsnMock string + dsnDefaultEngineMock string + dsnDefaultAccountMock string + usernameMock string + passwordMock string + databaseMock string + engineUrlMock string + engineNameMock string + accountNameMock string + clientMock *Client ) +// init populates mock variables and client for integration tests func init() { - username = os.Getenv("USER_NAME") - password = os.Getenv("PASSWORD") - database = os.Getenv("DATABASE_NAME") - engineName = os.Getenv("ENGINE_NAME") - engineUrl = os.Getenv("ENGINE_URL") - accountName = os.Getenv("ACCOUNT_NAME") - - dsn = fmt.Sprintf("firebolt://%s:%s@%s/%s?account_name=%s", username, password, database, engineName, accountName) - dsnDefaultEngine = fmt.Sprintf("firebolt://%s:%s@%s?account_name=%s", username, password, database, accountName) - dsnDefaultAccount = fmt.Sprintf("firebolt://%s:%s@%s", username, password, database) + usernameMock = os.Getenv("USER_NAME") + passwordMock = os.Getenv("PASSWORD") + databaseMock = os.Getenv("DATABASE_NAME") + engineNameMock = os.Getenv("ENGINE_NAME") + engineUrlMock = os.Getenv("ENGINE_URL") + accountNameMock = os.Getenv("ACCOUNT_NAME") + + dsnMock = fmt.Sprintf("firebolt://%s:%s@%s/%s?account_name=%s", usernameMock, passwordMock, databaseMock, engineNameMock, accountNameMock) + dsnDefaultEngineMock = fmt.Sprintf("firebolt://%s:%s@%s?account_name=%s", usernameMock, passwordMock, databaseMock, accountNameMock) + dsnDefaultAccountMock = fmt.Sprintf("firebolt://%s:%s@%s", usernameMock, passwordMock, databaseMock) + + clientMock, _ = Authenticate(usernameMock, passwordMock) +} + +// Calling this function would skip it, if the short flag is being raised +func markIntegrationTest(t *testing.T) { + if testing.Short() { + t.Skip() + } } +// TestDriverOpen tests that the driver is opened (happy path) func TestDriverOpen(t *testing.T) { db, err := sql.Open("firebolt", "firebolt://user:pass@db_name") if err != nil { @@ -43,6 +57,48 @@ func TestDriverOpen(t *testing.T) { } } +// TestDriverQueryResult tests query happy path, as user would do it +func TestDriverQueryResult(t *testing.T) { + markIntegrationTest(t) + + loc, _ := time.LoadLocation("UTC") + + db, err := sql.Open("firebolt", dsnMock) + if err != nil { + t.Errorf("failed unexpectedly with %v", err) + } + rows, err := db.Query( + "SELECT CAST('2020-01-03 19:08:45' AS DATETIME) as dt, CAST('2020-01-03' AS DATE) as d, CAST(1 AS INT) as i " + + "UNION " + + "SELECT CAST('2021-01-03 19:38:34' AS DATETIME) as dt, CAST('2000-12-03' AS DATE) as d, CAST(2 AS INT) as i ORDER BY i") + if err != nil { + t.Errorf("db.Query returned an error: %v", err) + } + var dt, d time.Time + var i int + + expectedColumns := []string{"dt", "d", "i"} + if columns, err := rows.Columns(); reflect.DeepEqual(expectedColumns, columns) && err != nil { + t.Errorf("columns are not equal (%v != %v) and error is %v", expectedColumns, columns, err) + } + + assert(rows.Next(), t, "Next returned end of output") + assert(rows.Scan(&dt, &d, &i) == nil, t, "Scan returned an error") + assert(dt == time.Date(2020, 01, 03, 19, 8, 45, 0, loc), t, "results not equal for datetime") + assert(d == time.Date(2020, 01, 03, 0, 0, 0, 0, loc), t, "results not equal for date") + assert(i == 1, t, "results not equal for int") + + assert(rows.Next(), t, "Next returned end of output") + assert(rows.Scan(&dt, &d, &i) == nil, t, "Scan returned an error") + assert(dt == time.Date(2021, 01, 03, 19, 38, 34, 0, loc), t, "results not equal for datetime") + assert(d == time.Date(2000, 12, 03, 0, 0, 0, 0, loc), t, "results not equal for date") + assert(i == 2, t, "results not equal for int") + + assert(!rows.Next(), t, "Next didn't returned false, although no data is expected") + +} + +// TestDriverOpenFail tests opening a driver with wrong dsn func TestDriverOpenFail(t *testing.T) { db, _ := sql.Open("firebolt", "firebolt://pass@db_name") ctx := context.TODO() @@ -52,12 +108,11 @@ func TestDriverOpenFail(t *testing.T) { } } +// TestDriverOpenConnection checks making a connection on opened driver func TestDriverOpenConnection(t *testing.T) { - if testing.Short() { - t.Skip() - } + markIntegrationTest(t) - db, err := sql.Open("firebolt", dsn) + db, err := sql.Open("firebolt", dsnMock) if err != nil { t.Errorf("failed unexpectedly") } @@ -69,9 +124,7 @@ func TestDriverOpenConnection(t *testing.T) { } func runTestDriverExecStatement(t *testing.T, dsn string) { - if testing.Short() { - t.Skip() - } + markIntegrationTest(t) db, err := sql.Open("firebolt", dsn) if err != nil { @@ -83,10 +136,12 @@ func runTestDriverExecStatement(t *testing.T, dsn string) { } } +// TestDriverOpenDefaultEngine checks opening driver with a default engine func TestDriverOpenDefaultEngine(t *testing.T) { - runTestDriverExecStatement(t, dsnDefaultEngine) + runTestDriverExecStatement(t, dsnDefaultEngineMock) } +// TestDriverExecStatement checks exec with full dsn func TestDriverExecStatement(t *testing.T) { - runTestDriverExecStatement(t, dsn) + runTestDriverExecStatement(t, dsnMock) } diff --git a/dsn_test.go b/dsn_test.go index 8922ec1..0997b01 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -52,6 +52,7 @@ func TestDSNHappyPath(t *testing.T) { fireboltSettings{username: "user@fire:bolt.io", password: "passwo@rd", database: "db_name"}) } +// TestDSNFailed test different failure scenarios for ParseDSNString func TestDSNFailed(t *testing.T) { runDSNTestFail(t, "") runDSNTestFail(t, "firebolt://") @@ -60,3 +61,23 @@ func TestDSNFailed(t *testing.T) { runDSNTestFail(t, "firebolt://yury_db@dn_name") runDSNTestFail(t, "firebolt://yury_db:password@dn_name?account=fi") } + +func runTestSplitString(t *testing.T, str string, stopChars []uint8, expectedFirst, expectedSecond string) { + first, second := splitString(str, stopChars) + if first != expectedFirst { + t.Errorf("splitString result is not as expected: %s != %s", first, expectedFirst) + } + if second != expectedSecond { + t.Errorf("splitString result is not as expected: %s != %s", second, expectedSecond) + } +} + +//TestSplitString tests several possible scenarios for SplitString function +func TestSplitString(t *testing.T) { + runTestSplitString(t, "some_str", []uint8{}, "some_str", "") + runTestSplitString(t, "some_str", []uint8{'r'}, "some_st", "r") + runTestSplitString(t, "some_str", []uint8{'s', 'o', 'm'}, "", "some_str") + runTestSplitString(t, "", []uint8{'s', 'o', 'm'}, "", "") + runTestSplitString(t, "", []uint8{}, "", "") + runTestSplitString(t, "some_str", []uint8{'_'}, "some", "_str") +} diff --git a/result.go b/result.go index c4030c4..1bb263d 100644 --- a/result.go +++ b/result.go @@ -1,7 +1,6 @@ package fireboltgosdk type FireboltResult struct { - str string } // LastInsertId returns last inserted ID, not supported by firebolt diff --git a/result_test.go b/result_test.go index e23df38..d2b836a 100644 --- a/result_test.go +++ b/result_test.go @@ -2,10 +2,13 @@ package fireboltgosdk import "testing" +// TestResult check, that the dummy FireboltResult doesn't return errors func TestResult(t *testing.T) { - res := FireboltResult{""} - id, _ := res.LastInsertId() - if id != 0 { - t.Errorf("got %d want %d", id, 0) + res := FireboltResult{} + if _, err := res.LastInsertId(); err != nil { + t.Errorf("Result LastInsertId failed with %v", err) + } + if _, err := res.RowsAffected(); err != nil { + t.Errorf("Result RowsAffected failed with %v", err) } } diff --git a/rows_test.go b/rows_test.go index 31773f5..084ce65 100644 --- a/rows_test.go +++ b/rows_test.go @@ -25,6 +25,7 @@ func mockRows() driver.Rows { return &fireboltRows{response, 0} } +// TestRowsColumns checks, that correct column names are returned func TestRowsColumns(t *testing.T) { rows := mockRows() @@ -34,6 +35,7 @@ func TestRowsColumns(t *testing.T) { } } +// TestRowsClose checks Close method, and inability to use rows afterward func TestRowsClose(t *testing.T) { rows := mockRows() if rows.Close() != nil { @@ -46,6 +48,7 @@ func TestRowsClose(t *testing.T) { } } +// TestRowsNext check Next method func TestRowsNext(t *testing.T) { rows := mockRows() var dest = make([]driver.Value, 10) diff --git a/statement_test.go b/statement_test.go index 6dbeaed..fe57a3f 100644 --- a/statement_test.go +++ b/statement_test.go @@ -7,48 +7,35 @@ import ( "testing" ) +// TestExecStmt checks simple SELECT 1 exec func TestExecStmt(t *testing.T) { - if testing.Short() { - t.Skip() - } - client, err := Authenticate(username, password) - if err != nil { - t.Errorf("auth failed with %v", err) - } + markIntegrationTest(t) - stmt := fireboltStmt{client: client, query: "SELECT 1", engineUrl: engineUrl, databaseName: database} - _, err = stmt.Exec(nil) - if err != nil { + stmt := fireboltStmt{client: clientMock, query: "SELECT 1", engineUrl: engineUrlMock, databaseName: databaseMock} + if _, err := stmt.Exec(nil); err != nil { t.Errorf("firebolt statement failed with %v", err) } } +// TestExecWrongStmt checks, that an error is returned on wrong query func TestExecWrongStmt(t *testing.T) { - if testing.Short() { - t.Skip() - } - client, err := Authenticate(username, password) - if err != nil { - t.Errorf("auth failed with %v", err) - } + markIntegrationTest(t) - stmt := fireboltStmt{client: client, query: "INSERT INTO", engineUrl: engineUrl, databaseName: database} - _, err = stmt.Exec(nil) - if err == nil { + stmt := fireboltStmt{client: clientMock, query: "INSERT INTO", engineUrl: engineUrlMock, databaseName: databaseMock} + if _, err := stmt.Exec(nil); err == nil { t.Errorf("firebolt statement didn't fail, but should") } } +// TestQueryStmt checks simple SELECT query func TestQueryStmt(t *testing.T) { - if testing.Short() { - t.Skip() - } - client, err := Authenticate(username, password) - if err != nil { - t.Errorf("auth failed with %v", err) - } + markIntegrationTest(t) - stmt := fireboltStmt{client: client, query: "SELECT 3213212 as \"const\", 2.3 as \"float\", 'some_text' as \"text\"", engineUrl: engineUrl, databaseName: database} + stmt := fireboltStmt{client: clientMock, + query: "SELECT 3213212 as \"const\", 2.3 as \"float\", 'some_text' as \"text\"", + engineUrl: engineUrlMock, + databaseName: databaseMock, + } rows, err := stmt.Query(nil) if err != nil { t.Errorf("firebolt statement failed with %v", err)