From e80a739d7fa9a7036f9b440c4e454ebae48b0ab3 Mon Sep 17 00:00:00 2001 From: Piotr Fus Date: Mon, 3 Feb 2025 11:42:14 +0100 Subject: [PATCH] SNOW-1825476 Implement programmatic access token (PAT) --- .github/workflows/build-test.yml | 14 +- auth.go | 15 ++ auth_test.go | 57 +++++ ci/test.bat | 3 + ci/test.sh | 3 + cmd/programmatic_access_token/.gitignore | 1 + cmd/programmatic_access_token/Makefile | 16 ++ cmd/programmatic_access_token/pat.go | 53 +++++ cmd/select1/select1.go | 3 +- driver.go | 3 + dsn.go | 11 +- dsn_test.go | 30 ++- .../mappings/auth/pat/invalid_token.json | 39 ++++ .../mappings/auth/pat/successful_flow.json | 70 ++++++ test_data/wiremock/mappings/select1.json | 204 ++++++++++++++++++ wiremock_test.go | 71 ++++++ 16 files changed, 586 insertions(+), 7 deletions(-) create mode 100644 cmd/programmatic_access_token/.gitignore create mode 100644 cmd/programmatic_access_token/Makefile create mode 100644 cmd/programmatic_access_token/pat.go create mode 100644 test_data/wiremock/mappings/auth/pat/invalid_token.json create mode 100644 test_data/wiremock/mappings/auth/pat/successful_flow.json create mode 100644 test_data/wiremock/mappings/select1.json create mode 100644 wiremock_test.go diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6d6216576..7569e5025 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -26,8 +26,6 @@ concurrency: jobs: lint: runs-on: ubuntu-latest - strategy: - fail-fast: false name: Check linter steps: - uses: actions/checkout@v4 @@ -52,6 +50,10 @@ jobs: name: ${{ matrix.cloud }} Go ${{ matrix.go }} on Ubuntu steps: - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 # for wiremock + with: + java-version: 11 + distribution: 'temurin' - name: Setup go uses: actions/setup-go@v5 with: @@ -78,6 +80,10 @@ jobs: name: ${{ matrix.cloud }} Go ${{ matrix.go }} on Mac steps: - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 # for wiremock + with: + java-version: 11 + distribution: 'temurin' - name: Setup go uses: actions/setup-go@v5 with: @@ -103,6 +109,10 @@ jobs: name: ${{ matrix.cloud }} Go ${{ matrix.go }} on Windows steps: - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 # for wiremock + with: + java-version: 11 + distribution: 'temurin' - name: Setup go uses: actions/setup-go@v5 with: diff --git a/auth.go b/auth.go index a95b968ba..17fad520a 100644 --- a/auth.go +++ b/auth.go @@ -51,6 +51,8 @@ const ( AuthTypeTokenAccessor // AuthTypeUsernamePasswordMFA is to use username and password with mfa AuthTypeUsernamePasswordMFA + // AuthTypePat is to use programmatic access token + AuthTypePat ) func determineAuthenticatorType(cfg *Config, value string) error { @@ -74,6 +76,9 @@ func determineAuthenticatorType(cfg *Config, value string) error { } else if upperCaseValue == AuthTypeTokenAccessor.String() { cfg.Authenticator = AuthTypeTokenAccessor return nil + } else if upperCaseValue == AuthTypePat.String() { + cfg.Authenticator = AuthTypePat + return nil } else { // possibly Okta case oktaURLString, err := url.QueryUnescape(lowerCaseValue) @@ -123,6 +128,8 @@ func (authType AuthType) String() string { return "TOKENACCESSOR" case AuthTypeUsernamePasswordMFA: return "USERNAME_PASSWORD_MFA" + case AuthTypePat: + return "PROGRAMMATIC_ACCESS_TOKEN" default: return "UNKNOWN" } @@ -442,6 +449,14 @@ func createRequestBody(sc *snowflakeConn, sessionParameters map[string]interface return nil, err } requestMain.Token = jwtTokenString + case AuthTypePat: + logger.WithContext(sc.ctx).Info("Programmatic access token") + requestMain.Authenticator = AuthTypePat.String() + requestMain.LoginName = sc.cfg.User + requestMain.Token = sc.cfg.Token + if sc.cfg.Password != "" && sc.cfg.Token == "" { + requestMain.Token = sc.cfg.Password + } case AuthTypeSnowflake: logger.WithContext(sc.ctx).Info("Username and password") requestMain.LoginName = sc.cfg.User diff --git a/auth_test.go b/auth_test.go index c559a2407..03123b11f 100644 --- a/auth_test.go +++ b/auth_test.go @@ -1003,3 +1003,60 @@ func TestContextPropagatedToAuthWhenUsingOpenDB(t *testing.T) { assertStringContainsE(t, err.Error(), "context deadline exceeded") cancel() } + +func TestPatSuccessfulFlow(t *testing.T) { + wiremock.registerMappings(t, + wiremockMapping{filePath: "auth/pat/successful_flow.json"}, + wiremockMapping{filePath: "select1.json", params: map[string]string{ + "%AUTHORIZATION_HEADER%": "Snowflake Token=\\\"session token\\\""}, + }, + ) + cfg := wiremock.connectionConfig() + cfg.Authenticator = AuthTypePat + cfg.Token = "some PAT" + connector := NewConnector(SnowflakeDriver{}, *cfg) + db := sql.OpenDB(connector) + rows, err := db.Query("SELECT 1") + assertNilF(t, err) + var v int + assertTrueE(t, rows.Next()) + assertNilF(t, rows.Scan(&v)) + assertEqualE(t, v, 1) +} + +func TestPatSuccessfulFlowWithPatAsPasswordWithPatAuthenticator(t *testing.T) { + wiremock.registerMappings(t, + wiremockMapping{filePath: "auth/pat/successful_flow.json"}, + wiremockMapping{filePath: "select1.json", params: map[string]string{ + "%AUTHORIZATION_HEADER%": "Snowflake Token=\\\"session token\\\""}, + }, + ) + cfg := wiremock.connectionConfig() + cfg.Authenticator = AuthTypePat + cfg.Password = "some PAT" + connector := NewConnector(SnowflakeDriver{}, *cfg) + db := sql.OpenDB(connector) + rows, err := db.Query("SELECT 1") + assertNilF(t, err) + var v int + assertTrueE(t, rows.Next()) + assertNilF(t, rows.Scan(&v)) + assertEqualE(t, v, 1) +} + +func TestPatInvalidToken(t *testing.T) { + wiremock.registerMappings(t, + wiremockMapping{filePath: "auth/pat/invalid_token.json"}, + ) + cfg := wiremock.connectionConfig() + cfg.Authenticator = AuthTypePat + cfg.Token = "some PAT" + connector := NewConnector(SnowflakeDriver{}, *cfg) + db := sql.OpenDB(connector) + _, err := db.Query("SELECT 1") + assertNotNilF(t, err) + var se *SnowflakeError + assertTrueF(t, errors.As(err, &se)) + assertEqualE(t, se.Number, 394400) + assertEqualE(t, se.Message, "Programmatic access token is invalid.") +} diff --git a/ci/test.bat b/ci/test.bat index c8343b75a..691313ed9 100644 --- a/ci/test.bat +++ b/ci/test.bat @@ -4,6 +4,9 @@ setlocal EnableDelayedExpansion start /b python ci\scripts\hang_webserver.py 12345 +curl -O https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/3.11.0/wiremock-standalone-3.11.0.jar +START /B java -jar wiremock-standalone-3.11.0.jar + if "%CLOUD_PROVIDER%"=="AWS" set PARAMETER_FILENAME=parameters_aws_golang.json.gpg if "%CLOUD_PROVIDER%"=="AZURE" set PARAMETER_FILENAME=parameters_azure_golang.json.gpg if "%CLOUD_PROVIDER%"=="GCP" set PARAMETER_FILENAME=parameters_gcp_golang.json.gpg diff --git a/ci/test.sh b/ci/test.sh index efbd1de0e..f43a2b9f6 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -7,6 +7,9 @@ set -o pipefail CI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +wget https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/3.11.0/wiremock-standalone-3.11.0.jar +java -jar wiremock-standalone-3.11.0.jar & + if [[ -n "$JENKINS_HOME" ]]; then ROOT_DIR="$(cd "${CI_DIR}/.." && pwd)" export WORKSPACE=${WORKSPACE:-/tmp} diff --git a/cmd/programmatic_access_token/.gitignore b/cmd/programmatic_access_token/.gitignore new file mode 100644 index 000000000..8edb9209e --- /dev/null +++ b/cmd/programmatic_access_token/.gitignore @@ -0,0 +1 @@ +pat diff --git a/cmd/programmatic_access_token/Makefile b/cmd/programmatic_access_token/Makefile new file mode 100644 index 000000000..6b4235665 --- /dev/null +++ b/cmd/programmatic_access_token/Makefile @@ -0,0 +1,16 @@ +include ../../gosnowflake.mak +CMD_TARGET=pat + +## Install +install: cinstall + +## Run +run: crun + +## Lint +lint: clint + +## Format source codes +fmt: cfmt + +.PHONY: install run lint fmt diff --git a/cmd/programmatic_access_token/pat.go b/cmd/programmatic_access_token/pat.go new file mode 100644 index 000000000..4b266b1c3 --- /dev/null +++ b/cmd/programmatic_access_token/pat.go @@ -0,0 +1,53 @@ +// you have to configure PAT on your user + +package main + +import ( + "database/sql" + "flag" + "fmt" + sf "github.com/snowflakedb/gosnowflake" + "log" +) + +func main() { + if !flag.Parsed() { + flag.Parse() + } + + cfg, err := sf.GetConfigFromEnv([]*sf.ConfigParam{ + {Name: "Account", EnvName: "SNOWFLAKE_TEST_ACCOUNT", FailOnMissing: true}, + {Name: "User", EnvName: "SNOWFLAKE_TEST_USER", FailOnMissing: true}, + {Name: "Password", EnvName: "SNOWFLAKE_TEST_PAT", FailOnMissing: false}, + //{Name: "Token", EnvName: "SNOWFLAKE_TEST_PAT", FailOnMissing: true}, + {Name: "Host", EnvName: "SNOWFLAKE_TEST_HOST", FailOnMissing: false}, + {Name: "Port", EnvName: "SNOWFLAKE_TEST_PORT", FailOnMissing: false}, + {Name: "Protocol", EnvName: "SNOWFLAKE_TEST_PROTOCOL", FailOnMissing: false}, + }) + cfg.Authenticator = sf.AuthTypePat + if err != nil { + log.Fatalf("cannot build config. %v", err) + } + + connector := sf.NewConnector(sf.SnowflakeDriver{}, *cfg) + db := sql.OpenDB(connector) + defer db.Close() + + query := "SELECT 1" + rows, err := db.Query(query) + if err != nil { + log.Fatalf("failed to run a query. %v, err: %v", query, err) + } + defer rows.Close() + var v int + if !rows.Next() { + log.Fatalf("no rows returned") + } + if err = rows.Scan(&v); err != nil { + log.Fatalf("failed to scan rows. %v", err) + } + if v != 1 { + log.Fatalf("unexpected result, expected 1, got %v", v) + } + fmt.Printf("Congrats! You have successfully run %v with Snowflake DB!\n", query) +} diff --git a/cmd/select1/select1.go b/cmd/select1/select1.go index 6cd328b7f..99debd5e8 100644 --- a/cmd/select1/select1.go +++ b/cmd/select1/select1.go @@ -21,11 +21,12 @@ func main() { cfg, err := sf.GetConfigFromEnv([]*sf.ConfigParam{ {Name: "Account", EnvName: "SNOWFLAKE_TEST_ACCOUNT", FailOnMissing: true}, {Name: "User", EnvName: "SNOWFLAKE_TEST_USER", FailOnMissing: true}, - {Name: "Password", EnvName: "SNOWFLAKE_TEST_PASSWORD", FailOnMissing: true}, + {Name: "Token", EnvName: "SNOWFLAKE_TEST_PAT", FailOnMissing: true}, {Name: "Host", EnvName: "SNOWFLAKE_TEST_HOST", FailOnMissing: false}, {Name: "Port", EnvName: "SNOWFLAKE_TEST_PORT", FailOnMissing: false}, {Name: "Protocol", EnvName: "SNOWFLAKE_TEST_PROTOCOL", FailOnMissing: false}, }) + cfg.Authenticator = sf.AuthTypePat if err != nil { log.Fatalf("failed to create Config, err: %v", err) } diff --git a/driver.go b/driver.go index 7fee0bd2a..1b95b6c9b 100644 --- a/driver.go +++ b/driver.go @@ -47,6 +47,9 @@ func (d SnowflakeDriver) OpenWithConfig(ctx context.Context, config Config) (dri if err := config.Validate(); err != nil { return nil, err } + if config.Params == nil { + config.Params = make(map[string]*string) + } if config.Tracing != "" { if err := logger.SetLogLevel(config.Tracing); err != nil { return nil, err diff --git a/dsn.go b/dsn.go index 8f304726b..f65bcc30f 100644 --- a/dsn.go +++ b/dsn.go @@ -576,14 +576,16 @@ func buildHostFromAccountAndRegion(account, region string) string { func authRequiresUser(cfg *Config) bool { return cfg.Authenticator != AuthTypeOAuth && cfg.Authenticator != AuthTypeTokenAccessor && - cfg.Authenticator != AuthTypeExternalBrowser + cfg.Authenticator != AuthTypeExternalBrowser && + cfg.Authenticator != AuthTypePat } func authRequiresPassword(cfg *Config) bool { return cfg.Authenticator != AuthTypeOAuth && cfg.Authenticator != AuthTypeTokenAccessor && cfg.Authenticator != AuthTypeExternalBrowser && - cfg.Authenticator != AuthTypeJwt + cfg.Authenticator != AuthTypeJwt && + cfg.Authenticator != AuthTypePat } // transformAccountToHost transforms account to host @@ -905,7 +907,7 @@ type ConfigParam struct { // GetConfigFromEnv is used to parse the environment variable values to specific fields of the Config func GetConfigFromEnv(properties []*ConfigParam) (*Config, error) { - var account, user, password, role, host, portStr, protocol, warehouse, database, schema, region, passcode, application string + var account, user, password, token, role, host, portStr, protocol, warehouse, database, schema, region, passcode, application string var privateKey *rsa.PrivateKey var err error if len(properties) == 0 || properties == nil { @@ -923,6 +925,8 @@ func GetConfigFromEnv(properties []*ConfigParam) (*Config, error) { user = value case "Password": password = value + case "Token": + token = value case "Role": role = value case "Host": @@ -963,6 +967,7 @@ func GetConfigFromEnv(properties []*ConfigParam) (*Config, error) { Account: account, User: user, Password: password, + Token: token, Role: role, Host: host, Port: port, diff --git a/dsn_test.go b/dsn_test.go index 9052b5c19..2eee74d74 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -1041,6 +1041,24 @@ func TestParseDSN(t *testing.T) { ocspMode: ocspModeFailOpen, err: nil, }, + { + dsn: "u:p@a.snowflake.local:9876?account=a&protocol=http&authenticator=PROGRAMMATIC_ACCESS_TOKEN&disableSamlURLCheck=false", + config: &Config{ + Account: "a", User: "u", Password: "p", + Authenticator: AuthTypePat, + Protocol: "http", Host: "a.snowflake.local", Port: 9876, + OCSPFailOpen: OCSPFailOpenTrue, + ValidateDefaultParameters: ConfigBoolTrue, + ClientTimeout: defaultClientTimeout, + JWTClientTimeout: defaultJWTClientTimeout, + ExternalBrowserTimeout: defaultExternalBrowserTimeout, + CloudStorageTimeout: defaultCloudStorageTimeout, + IncludeRetryReason: ConfigBoolTrue, + DisableSamlURLCheck: ConfigBoolFalse, + }, + ocspMode: ocspModeFailOpen, + err: nil, + }, } for _, at := range []AuthType{AuthTypeExternalBrowser, AuthTypeOAuth} { @@ -1064,7 +1082,7 @@ func TestParseDSN(t *testing.T) { }) } - for _, at := range []AuthType{AuthTypeSnowflake, AuthTypeUsernamePasswordMFA, AuthTypeJwt} { + for _, at := range []AuthType{AuthTypeSnowflake, AuthTypeUsernamePasswordMFA, AuthTypeJwt, AuthTypePat} { testcases = append(testcases, tcParseDSN{ dsn: fmt.Sprintf("@host:888/db/schema?account=ac&protocol=http&authenticator=%v", strings.ToLower(at.String())), config: &Config{ @@ -1465,6 +1483,16 @@ func TestDSN(t *testing.T) { }, dsn: "u:p@a.snowflakecomputing.com:443?authenticator=externalbrowser&clientStoreTemporaryCredential=false&ocspFailOpen=true&validateDefaultParameters=true", }, + { + cfg: &Config{ + User: "u", + Password: "p", + Account: "a", + Authenticator: AuthTypePat, + ClientStoreTemporaryCredential: ConfigBoolFalse, + }, + dsn: "u:p@a.snowflakecomputing.com:443?authenticator=programmatic_access_token&clientStoreTemporaryCredential=false&ocspFailOpen=true&validateDefaultParameters=true", + }, { cfg: &Config{ User: "u", diff --git a/test_data/wiremock/mappings/auth/pat/invalid_token.json b/test_data/wiremock/mappings/auth/pat/invalid_token.json new file mode 100644 index 000000000..9a4edaba5 --- /dev/null +++ b/test_data/wiremock/mappings/auth/pat/invalid_token.json @@ -0,0 +1,39 @@ +{ + "mappings": [ + { + "scenarioName": "Successful PAT authentication flow", + "requiredScenarioState": "Started", + "newScenarioState": "Authenticated", + "request": { + "urlPathPattern": "/session/v1/login-request.*", + "method": "POST", + "bodyPatterns": [ + { + "equalToJson" : { + "data": { + "LOGIN_NAME": "testUser", + "AUTHENTICATOR": "PROGRAMMATIC_ACCESS_TOKEN", + "TOKEN": "some PAT" + } + }, + "ignoreExtraElements" : true + } + ] + }, + "response": { + "status": 200, + "jsonBody": { + "data": { + "nextAction": "RETRY_LOGIN", + "authnMethod": "PAT", + "signInOptions": {} + }, + "code": "394400", + "message": "Programmatic access token is invalid.", + "success": false, + "headers": null + } + } + } + ] +} \ No newline at end of file diff --git a/test_data/wiremock/mappings/auth/pat/successful_flow.json b/test_data/wiremock/mappings/auth/pat/successful_flow.json new file mode 100644 index 000000000..66ac97454 --- /dev/null +++ b/test_data/wiremock/mappings/auth/pat/successful_flow.json @@ -0,0 +1,70 @@ +{ + "mappings": [ + { + "scenarioName": "Successful PAT authentication flow", + "requiredScenarioState": "Started", + "newScenarioState": "Authenticated", + "request": { + "urlPathPattern": "/session/v1/login-request.*", + "method": "POST", + "bodyPatterns": [ + { + "equalToJson" : { + "data": { + "LOGIN_NAME": "testUser", + "AUTHENTICATOR": "PROGRAMMATIC_ACCESS_TOKEN", + "TOKEN": "some PAT" + } + }, + "ignoreExtraElements" : true + }, + { + "matchesJsonPath": { + "expression": "$.data.PASSWORD", + "absent": "(absent)" + } + } + ] + }, + "response": { + "status": 200, + "jsonBody": { + "data": { + "masterToken": "master token", + "token": "session token", + "validityInSeconds": 3600, + "masterValidityInSeconds": 14400, + "displayUserName": "OAUTH_TEST_AUTH_CODE", + "serverVersion": "8.48.0 b2024121104444034239f05", + "firstLogin": false, + "remMeToken": null, + "remMeValidityInSeconds": 0, + "healthCheckInterval": 45, + "newClientForUpgrade": "3.12.3", + "sessionId": 1172562260498, + "parameters": [ + { + "name": "CLIENT_PREFETCH_THREADS", + "value": 4 + } + ], + "sessionInfo": { + "databaseName": "TEST_DHEYMAN", + "schemaName": "TEST_JDBC", + "warehouseName": "TEST_XSMALL", + "roleName": "ANALYST" + }, + "idToken": null, + "idTokenValidityInSeconds": 0, + "responseData": null, + "mfaToken": null, + "mfaTokenValidityInSeconds": 0 + }, + "code": null, + "message": null, + "success": true + } + } + } + ] +} \ No newline at end of file diff --git a/test_data/wiremock/mappings/select1.json b/test_data/wiremock/mappings/select1.json new file mode 100644 index 000000000..4de4d4c48 --- /dev/null +++ b/test_data/wiremock/mappings/select1.json @@ -0,0 +1,204 @@ +{ + "mappings": [ + { + "scenarioName": "Successful SELECT 1 flow", + "request": { + "urlPathPattern": "/queries/v1/query-request.*", + "method": "POST", + "headers": { + "Authorization": { + "equalTo": "%AUTHORIZATION_HEADER%" + } + } + }, + "response": { + "status": 200, + "jsonBody": { + "data": { + "parameters": [ + { + "name": "TIMESTAMP_OUTPUT_FORMAT", + "value": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM" + }, + { + "name": "CLIENT_PREFETCH_THREADS", + "value": 4 + }, + { + "name": "TIME_OUTPUT_FORMAT", + "value": "HH24:MI:SS" + }, + { + "name": "CLIENT_RESULT_CHUNK_SIZE", + "value": 16 + }, + { + "name": "TIMESTAMP_TZ_OUTPUT_FORMAT", + "value": "" + }, + { + "name": "CLIENT_SESSION_KEEP_ALIVE", + "value": false + }, + { + "name": "QUERY_CONTEXT_CACHE_SIZE", + "value": 5 + }, + { + "name": "CLIENT_METADATA_USE_SESSION_DATABASE", + "value": false + }, + { + "name": "CLIENT_OUT_OF_BAND_TELEMETRY_ENABLED", + "value": false + }, + { + "name": "ENABLE_STAGE_S3_PRIVATELINK_FOR_US_EAST_1", + "value": true + }, + { + "name": "TIMESTAMP_NTZ_OUTPUT_FORMAT", + "value": "YYYY-MM-DD HH24:MI:SS.FF3" + }, + { + "name": "CLIENT_RESULT_PREFETCH_THREADS", + "value": 1 + }, + { + "name": "CLIENT_METADATA_REQUEST_USE_CONNECTION_CTX", + "value": false + }, + { + "name": "CLIENT_HONOR_CLIENT_TZ_FOR_TIMESTAMP_NTZ", + "value": true + }, + { + "name": "CLIENT_MEMORY_LIMIT", + "value": 1536 + }, + { + "name": "CLIENT_TIMESTAMP_TYPE_MAPPING", + "value": "TIMESTAMP_LTZ" + }, + { + "name": "TIMEZONE", + "value": "America/Los_Angeles" + }, + { + "name": "SERVICE_NAME", + "value": "" + }, + { + "name": "CLIENT_RESULT_PREFETCH_SLOTS", + "value": 2 + }, + { + "name": "CLIENT_TELEMETRY_ENABLED", + "value": true + }, + { + "name": "CLIENT_DISABLE_INCIDENTS", + "value": true + }, + { + "name": "CLIENT_USE_V1_QUERY_API", + "value": true + }, + { + "name": "CLIENT_RESULT_COLUMN_CASE_INSENSITIVE", + "value": false + }, + { + "name": "CSV_TIMESTAMP_FORMAT", + "value": "" + }, + { + "name": "BINARY_OUTPUT_FORMAT", + "value": "HEX" + }, + { + "name": "CLIENT_ENABLE_LOG_INFO_STATEMENT_PARAMETERS", + "value": false + }, + { + "name": "CLIENT_TELEMETRY_SESSIONLESS_ENABLED", + "value": true + }, + { + "name": "DATE_OUTPUT_FORMAT", + "value": "YYYY-MM-DD" + }, + { + "name": "CLIENT_STAGE_ARRAY_BINDING_THRESHOLD", + "value": 65280 + }, + { + "name": "CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY", + "value": 3600 + }, + { + "name": "CLIENT_SESSION_CLONE", + "value": false + }, + { + "name": "AUTOCOMMIT", + "value": true + }, + { + "name": "TIMESTAMP_LTZ_OUTPUT_FORMAT", + "value": "" + } + ], + "rowtype": [ + { + "name": "1", + "database": "", + "schema": "", + "table": "", + "nullable": false, + "length": null, + "type": "fixed", + "scale": 0, + "precision": 1, + "byteLength": null, + "collation": null + } + ], + "rowset": [ + [ + "1" + ] + ], + "total": 1, + "returned": 1, + "queryId": "01ba13b4-0104-e9fd-0000-0111029ca00e", + "databaseProvider": null, + "finalDatabaseName": null, + "finalSchemaName": null, + "finalWarehouseName": "TEST_XSMALL", + "finalRoleName": "ACCOUNTADMIN", + "numberOfBinds": 0, + "arrayBindSupported": false, + "statementTypeId": 4096, + "version": 1, + "sendResultTime": 1738317395581, + "queryResultFormat": "json", + "queryContext": { + "entries": [ + { + "id": 0, + "timestamp": 1738317395574564, + "priority": 0, + "context": "CPbPTg==" + } + ] + } + }, + "code": null, + "message": null, + "success": true + } + } + } + ] +} \ No newline at end of file diff --git a/wiremock_test.go b/wiremock_test.go new file mode 100644 index 000000000..3b03b65aa --- /dev/null +++ b/wiremock_test.go @@ -0,0 +1,71 @@ +package gosnowflake + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" + "testing" +) + +var wiremock *wiremockClient = newWiremock() + +type wiremockClient struct { + host string + port int + client http.Client +} + +func newWiremock() *wiremockClient { + return &wiremockClient{ + host: "localhost", + port: 8080, + } +} + +func (wm *wiremockClient) connectionConfig() *Config { + return &Config{ + User: "testUser", + Host: wm.host, + Port: wm.port, + Account: "testAccount", + Protocol: "http", + } +} + +type wiremockMapping struct { + filePath string + params map[string]string +} + +func (wm *wiremockClient) registerMappings(t *testing.T, mappings ...wiremockMapping) { + for _, mapping := range mappings { + f, err := os.Open("test_data/wiremock/mappings/" + mapping.filePath) + assertNilF(t, err) + defer f.Close() + mappingBodyBytes, err := io.ReadAll(f) + mappingBody := string(mappingBodyBytes) + assertNilF(t, err) + for key, val := range mapping.params { + mappingBody = strings.Replace(mappingBody, key, val, 1) + } + resp, err := wm.client.Post(fmt.Sprintf("%v/import", wm.mappingsURL()), "application/json", strings.NewReader(mappingBody)) + assertNilF(t, err) + if resp.StatusCode != http.StatusOK { + respBody, err := io.ReadAll(resp.Body) + assertNilF(t, err) + t.Fatalf("cannot create mapping.\n%v", string(respBody)) + } + } + t.Cleanup(func() { + req, err := http.NewRequest("DELETE", wm.mappingsURL(), nil) + assertNilE(t, err) + _, err = wm.client.Do(req) + assertNilE(t, err) + }) +} + +func (wm *wiremockClient) mappingsURL() string { + return fmt.Sprintf("http://%v:%v/__admin/mappings", wm.host, wm.port) +}