diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index c0f496c0e54..0076cc3168a 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -41,6 +41,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima ** JavaScript: adopted `undici` for the default dispatcher; renamed `reader`->`responseSerializer`; added `readTimeoutMillis`, `keepAliveTimeMillis`, `maxResponseHeaderBytes`, `proxy`, `compression`, `batchSize`, `bulkResults`, `logger`; removed `headers` (use interceptors) and `ca`/`cert`/`pfx`/`rejectUnauthorized`/`agent` (TLS via the Node/undici runtime); undici is swapped out in browser bundles. *(breaking)* ** All drivers: compression now defaults on; `connectTimeout` lowered to 5s and applied to transport establishment; `readTimeout` is a streaming-safe idle-read timeout (off by default); `idleTimeout` reaps only pooled connections. * Changed all GLV drivers to always serialize requests as JSON (`application/json`). Request serialization is no longer configurable and the serializer options (now `responseSerializer`) control only response deserialization. +* Fixed remote traversals in Java, Python, .NET, and JavaScript to avoid sending the traversal source alias as a GremlinLang parameter, and removed the unused GremlinLang traversal-source helpers. * Fixed `gremlin-javascript` `Client.submit()` so that an explicit `bulkResults: false` request option is forwarded to the server instead of being silently dropped. * Fixed `gremlin-dotnet` deflate response decompression, which threw on the server's zlib-framed output because it used `DeflateStream` (raw DEFLATE, RFC 1951) instead of `ZLibStream` (zlib, RFC 1950); the bug was previously masked because compression was off by default. * Fixed `gremlin-dotnet` SSL options cloning (used on the skip-certificate-validation path) to copy `ClientCertificateContext` and `AllowTlsResume`, which were previously dropped, breaking mTLS client certificates and silently re-enabling TLS resumption. diff --git a/docs/src/upgrade/release-4.x.x.asciidoc b/docs/src/upgrade/release-4.x.x.asciidoc index 11c89536fc5..dff15e5c031 100644 --- a/docs/src/upgrade/release-4.x.x.asciidoc +++ b/docs/src/upgrade/release-4.x.x.asciidoc @@ -65,6 +65,33 @@ RequestMessage.build("g.V(x)").addParameters(params).create(); See: link:https://issues.apache.org/jira/browse/TINKERPOP-3262[TINKERPOP-3262] +==== Traversal Source Not In Parameters + +The traversal source alias for an HTTP request is selected with the top-level `g` request field. Query `parameters` +are only the named values referenced by the Gremlin string. Earlier 3.8.x driver builds could place the traversal +source alias into `bindings` as `g` (reminder: `bindings` have been renamed to `parameters`). Gremlin Server will ignore +this "g" value in "parameters" so use the `g` field instead. + +Applications that construct HTTP requests directly should keep the traversal source alias out of `parameters` and set +the top-level `g` field instead: + +[source,text] +---- +// old +{ + "gremlin": "g.V(x)", + "parameters": "[\"g\":\"gmodern\",\"x\":1]" +} + +// new +{ + "gremlin": "g.V(x)", + "g": "gmodern", + "parameters": "[\"x\":1]" +} +---- + + ==== Standardizing GLV Connection Options TinkerPop 4.x standardizes connection option names and defaults across all five Gremlin Language Variants (Java, Python, diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java index af64ee02b9f..5bbe49e5aee 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java @@ -495,13 +495,6 @@ public static String convertParametersToString(final Map params) return sb.toString(); } - /** - * The alias to set. - */ - public void addG(final String g) { - parameters.put("g", g); - } - /** * Add a {@link TraversalSource} instruction to the GremlinLang. * diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs index c473260b5c9..3f3ef7462be 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs @@ -118,8 +118,6 @@ public async Task> SubmitAsync(GremlinLan CancellationToken cancellationToken = default) { _logger.SubmittingGremlinLang(gremlinLang); - gremlinLang.AddG(_traversalSource); - var requestMsg = RequestMessage.Build(gremlinLang.GetGremlin()) .AddG(_traversalSource); diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/TransactionRemoteConnection.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/TransactionRemoteConnection.cs index 404f4ffe00f..ac60914b5ab 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/TransactionRemoteConnection.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/TransactionRemoteConnection.cs @@ -60,8 +60,6 @@ public async Task> SubmitAsync(GremlinLan throw new InvalidOperationException("Transaction is not open"); } - gremlinLang.AddG(_traversalSource); - var requestMsg = RequestMessage.Build(gremlinLang.GetGremlin()) .AddG(_traversalSource); diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs index 629ac9fd89d..661004fd46b 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs @@ -91,15 +91,6 @@ public void AddStep(string stepName, params object?[] arguments) AddToGremlin(stepName, arguments); } - /// - /// Sets the alias for the traversal source. - /// - /// The alias to set. - public void AddG(string g) - { - _parameters["g"] = g; - } - /// /// Appends raw text to the gremlin string. Used for temporary options rendering. /// diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs index 02e78f83530..e4c1b1fb05f 100644 --- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs +++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/DriverRemoteConnectionTests.cs @@ -118,6 +118,26 @@ public async Task ShouldBuildRequestWithParameters() Assert.NotNull(capturedRequest); var parametersString = (string)capturedRequest!.Fields[Tokens.ArgsParameters]; Assert.Contains("\"x\":42", parametersString); + Assert.DoesNotContain("\"g\":", parametersString); + } + + [Fact] + public async Task ShouldNotBuildRequestWithTraversalSourceAsParameter() + { + RequestMessage? capturedRequest = null; + var client = CreateCapturingClient(msg => capturedRequest = msg); + var connection = new DriverRemoteConnection(client, "mySource"); + + var gl = new GremlinLang(); + gl.AddStep("V", Array.Empty()); + + await connection.SubmitAsync(gl); + + Assert.NotNull(capturedRequest); + var parametersString = capturedRequest!.Fields.TryGetValue(Tokens.ArgsParameters, out var parameters) + ? (string) parameters + : ""; + Assert.DoesNotContain("\"g\":", parametersString); } [Fact] diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java index 67715efba7b..809162c792c 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java @@ -240,7 +240,6 @@ public CompletableFuture> submitAsync(final GremlinLan } try { - gremlinLang.addG(remoteTraversalSourceName); return client.submitAsync(gremlinLang.getGremlin(), getRequestOptions(gremlinLang)) .thenApply(rs -> new DriverRemoteTraversal<>(rs, client, attachElements, conf)); } catch (Exception ex) { diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnectionTest.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnectionTest.java index a69eaf348a2..2d9e0a8d186 100644 --- a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnectionTest.java +++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnectionTest.java @@ -18,14 +18,30 @@ */ package org.apache.tinkerpop.gremlin.driver.remote; +import org.apache.tinkerpop.gremlin.driver.Client; import org.apache.tinkerpop.gremlin.driver.RequestOptions; -import org.apache.tinkerpop.gremlin.util.Tokens; +import org.apache.tinkerpop.gremlin.driver.ResultSet; +import org.apache.tinkerpop.gremlin.process.traversal.GremlinLang; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; +import org.apache.tinkerpop.gremlin.util.Tokens; import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Collections; +import java.util.concurrent.CompletableFuture; import static org.apache.tinkerpop.gremlin.driver.RequestOptions.getRequestOptions; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * @author Stephen Mallette (http://stephen.genoprime.com) @@ -55,4 +71,45 @@ public void shouldBuildRequestOptionsWithNumerics() { assertEquals(Integer.valueOf(100), options.getBatchSize().get()); assertEquals(Long.valueOf(1000), options.getTimeoutMillis().get()); } + + @Test + public void shouldSubmitTraversalWithoutTraversalSourceAsParameter() throws Exception { + final Client client = mockClient(); + final DriverRemoteConnection connection = DriverRemoteConnection.using(client, "mySource"); + + connection.submitAsync(g.V().asAdmin().getGremlinLang()).get(); + + final RequestOptions options = captureRequestOptions(client, "g.V()"); + assertFalse(options.getParameters().orElse("").contains("\"g\":")); + } + + @Test + public void shouldSubmitTraversalWithOnlyExplicitParameters() throws Exception { + final Client client = mockClient(); + final DriverRemoteConnection connection = DriverRemoteConnection.using(client, "mySource"); + final GremlinLang gremlinLang = g.V(GValue.of("x", 42)).asAdmin().getGremlinLang(); + + connection.submitAsync(gremlinLang).get(); + + final RequestOptions options = captureRequestOptions(client, "g.V(x)"); + assertTrue(options.getParameters().isPresent()); + assertTrue(options.getParameters().get().contains("\"x\":42")); + assertFalse(options.getParameters().get().contains("\"g\":")); + } + + private static Client mockClient() { + final Client client = mock(Client.class); + final ResultSet resultSet = mock(ResultSet.class); + when(resultSet.iterator()).thenReturn(Collections.emptyIterator()); + when(client.alias("mySource")).thenReturn(client); + when(client.submitAsync(anyString(), any(RequestOptions.class))) + .thenReturn(CompletableFuture.completedFuture(resultSet)); + return client; + } + + private static RequestOptions captureRequestOptions(final Client client, final String gremlin) { + final ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(RequestOptions.class); + verify(client).submitAsync(eq(gremlin), optionsCaptor.capture()); + return optionsCaptor.getValue(); + } } diff --git a/gremlin-go/driver/gremlinlang.go b/gremlin-go/driver/gremlinlang.go index e0533dcaf76..7e90cb341ea 100644 --- a/gremlin-go/driver/gremlinlang.go +++ b/gremlin-go/driver/gremlinlang.go @@ -554,10 +554,6 @@ func ConvertParametersToString(params map[string]interface{}) string { return sb.String() } -func (gl *GremlinLang) AddG(g string) { - gl.parameters["g"] = g -} - func (gl *GremlinLang) AddSource(name string, arguments ...interface{}) { if (name == "withStrategies" || name == "withoutStrategies") && len(arguments) != 0 { args := gl.buildStrategyArgs(arguments...) diff --git a/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts b/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts index 570adc95767..d61fde3fd40 100644 --- a/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts +++ b/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts @@ -76,8 +76,6 @@ export default class DriverRemoteConnection extends RemoteConnection { } #buildRequestArgs(gremlinLang: GremlinLang) { - gremlinLang.addG(this.options.traversalSource || 'g'); - let requestOptions: RequestOptions | undefined = undefined; const strategies = gremlinLang.getOptionsStrategies(); if (strategies.length > 0) { diff --git a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts index f718e9621ef..47cd08a28ac 100644 --- a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts +++ b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts @@ -51,10 +51,6 @@ export default class GremlinLang { this.gremlin += text; } - addG(g: string): void { - this.parameters.set('g', g); - } - getParameters(): Map { return this.parameters; } @@ -304,4 +300,4 @@ export default class GremlinLang { }); return '[' + parts.join(',') + ']'; } -} \ No newline at end of file +} diff --git a/gremlin-js/gremlin-javascript/test/unit/driver-remote-connection-test.js b/gremlin-js/gremlin-javascript/test/unit/driver-remote-connection-test.js new file mode 100644 index 00000000000..c4d5bbd13a2 --- /dev/null +++ b/gremlin-js/gremlin-javascript/test/unit/driver-remote-connection-test.js @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert'; +import DriverRemoteConnection from '../../lib/driver/driver-remote-connection.js'; +import GremlinLang from '../../lib/process/gremlin-lang.js'; + +describe('DriverRemoteConnection', function () { + function createConnection(capture) { + const connection = new DriverRemoteConnection('http://localhost:8182/gremlin', + { traversalSource: 'mySource', connectOnStartup: false }); + connection._client = { + stream(gremlin, parameters, requestOptions) { + capture({ gremlin, parameters, requestOptions }); + return (async function* () {})(); + }, + }; + return connection; + } + + it('does not send traversal source as a parameter', async function () { + let captured; + const connection = createConnection(args => captured = args); + const gremlinLang = new GremlinLang().addStep('V'); + + await connection.submit(gremlinLang); + + assert.ok(!captured.requestOptions.parameters || !captured.requestOptions.parameters.includes("'g':")); + }); + + it('sends explicit GremlinLang parameters without adding traversal source', async function () { + let captured; + const connection = createConnection(args => captured = args); + const gremlinLang = new GremlinLang().addStep('V'); + gremlinLang.getParameters().set('x', 42); + + await connection.submit(gremlinLang); + + assert.strictEqual(captured.requestOptions.parameters, "['x':42]"); + }); +}); diff --git a/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py b/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py index e911720a3a2..be0d92ed5b7 100644 --- a/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py +++ b/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py @@ -70,7 +70,6 @@ def close(self): def submit(self, gremlin_lang): log.debug("submit with gremlin lang script '%s'", gremlin_lang.get_gremlin()) - gremlin_lang.add_g(self._traversal_source) result_set = self._client.submit(gremlin_lang.get_gremlin(), request_options=self.extract_request_options(gremlin_lang)) return RemoteTraversal(result_set) @@ -85,7 +84,6 @@ def submitAsync(self, message, parameters=None, request_options=None): def submit_async(self, gremlin_lang): log.debug("submit_async with gremlin lang script '%s'", gremlin_lang.get_gremlin()) future = Future() - gremlin_lang.add_g(self._traversal_source) future_result_set = self._client.submit_async(gremlin_lang.get_gremlin(), request_options=self.extract_request_options(gremlin_lang)) diff --git a/gremlin-python/src/main/python/gremlin_python/driver/transaction.py b/gremlin-python/src/main/python/gremlin_python/driver/transaction.py index d83ded122bc..884b0db49a7 100644 --- a/gremlin-python/src/main/python/gremlin_python/driver/transaction.py +++ b/gremlin-python/src/main/python/gremlin_python/driver/transaction.py @@ -166,8 +166,6 @@ def submit(self, gremlin_lang): if not self._transaction.is_open: raise Exception("Transaction is not open") - gremlin_lang.add_g(self._traversal_source) - from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection request_options = DriverRemoteConnection.extract_request_options(gremlin_lang) request_options['transactionId'] = self._transaction.transaction_id diff --git a/gremlin-python/src/main/python/gremlin_python/process/traversal.py b/gremlin-python/src/main/python/gremlin_python/process/traversal.py index d8088e5779a..5adc335d338 100644 --- a/gremlin-python/src/main/python/gremlin_python/process/traversal.py +++ b/gremlin-python/src/main/python/gremlin_python/process/traversal.py @@ -1090,9 +1090,6 @@ def convert_parameters_to_string(params): parts.append(f'{helper._arg_as_string(k)}:{helper._arg_as_string(v)}') return '[' + ','.join(parts) + ']' - def add_g(self, g): - self.parameters['g'] = g - def add_source(self, source_name, *args): if source_name == 'withStrategies' and len(args) != 0: diff --git a/gremlin-python/src/main/python/tests/unit/driver/test_client_options.py b/gremlin-python/src/main/python/tests/unit/driver/test_client_options.py index 8434fab4cc2..a11473c9cf6 100644 --- a/gremlin-python/src/main/python/tests/unit/driver/test_client_options.py +++ b/gremlin-python/src/main/python/tests/unit/driver/test_client_options.py @@ -21,6 +21,7 @@ DriverRemoteConnection, and the SigV4 credentials-provider variant.""" import warnings +from concurrent.futures import Future from unittest.mock import MagicMock, patch import pytest @@ -28,6 +29,7 @@ from gremlin_python.driver.client import Client from gremlin_python.driver.connection import Connection from gremlin_python.driver.request import RequestMessage +from gremlin_python.process.traversal import GremlinLang, GValue # Patch Connection so Client._fill_pool does not attempt any real connections. @@ -197,6 +199,97 @@ def test_submit_async_signature_single_arg(self): # self + gremlin_lang only assert params == ['self', 'gremlin_lang'] + def test_submit_does_not_send_traversal_source_as_parameter(self): + from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection + with patch('gremlin_python.driver.driver_remote_connection.client.Client') as MockClient: + instance = MockClient.return_value + instance._url = 'http://localhost:8182/gremlin' + instance._traversal_source = 'mySource' + instance.submit.return_value = [] + connection = DriverRemoteConnection('http://localhost:8182/gremlin', 'mySource') + gremlin_lang = GremlinLang() + gremlin_lang.add_step('V') + + connection.submit(gremlin_lang) + + request_options = instance.submit.call_args.kwargs['request_options'] + assert "'g':" not in request_options.get('parameters', '') + + def test_submit_sends_only_explicit_parameters(self): + from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection + with patch('gremlin_python.driver.driver_remote_connection.client.Client') as MockClient: + instance = MockClient.return_value + instance._url = 'http://localhost:8182/gremlin' + instance._traversal_source = 'mySource' + instance.submit.return_value = [] + connection = DriverRemoteConnection('http://localhost:8182/gremlin', 'mySource') + gremlin_lang = GremlinLang() + gremlin_lang.add_step('V', GValue('x', 42)) + + connection.submit(gremlin_lang) + + request_options = instance.submit.call_args.kwargs['request_options'] + assert request_options['parameters'] == "['x':42]" + assert "'g':" not in request_options['parameters'] + + def test_submit_async_does_not_send_traversal_source_as_parameter(self): + from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection + with patch('gremlin_python.driver.driver_remote_connection.client.Client') as MockClient: + instance = MockClient.return_value + instance._url = 'http://localhost:8182/gremlin' + instance._traversal_source = 'mySource' + result = Future() + result.set_result([]) + instance.submit_async.return_value = result + connection = DriverRemoteConnection('http://localhost:8182/gremlin', 'mySource') + gremlin_lang = GremlinLang() + gremlin_lang.add_step('V') + + connection.submit_async(gremlin_lang).result() + + request_options = instance.submit_async.call_args.kwargs['request_options'] + assert "'g':" not in request_options.get('parameters', '') + + def test_transaction_submit_does_not_send_traversal_source_as_parameter(self): + from gremlin_python.driver.transaction import TransactionRemoteConnection + client = MagicMock() + client._url = 'http://localhost:8182/gremlin' + client._traversal_source = 'mySource' + client.submit.return_value = [] + transaction = MagicMock() + transaction._client = client + transaction.is_open = True + transaction.transaction_id = 'tx-1' + connection = TransactionRemoteConnection(transaction) + gremlin_lang = GremlinLang() + gremlin_lang.add_step('V') + + connection.submit(gremlin_lang) + + request_options = client.submit.call_args.kwargs['request_options'] + assert "'g':" not in request_options.get('parameters', '') + + def test_transaction_submit_sends_only_explicit_parameters(self): + from gremlin_python.driver.transaction import TransactionRemoteConnection + client = MagicMock() + client._url = 'http://localhost:8182/gremlin' + client._traversal_source = 'mySource' + client.submit.return_value = [] + transaction = MagicMock() + transaction._client = client + transaction.is_open = True + transaction.transaction_id = 'tx-1' + connection = TransactionRemoteConnection(transaction) + gremlin_lang = GremlinLang() + gremlin_lang.add_step('V', GValue('x', 42)) + + connection.submit(gremlin_lang) + + request_options = client.submit.call_args.kwargs['request_options'] + assert request_options['transactionId'] == 'tx-1' + assert request_options['parameters'] == "['x':42]" + assert "'g':" not in request_options['parameters'] + class TestSigV4CredentialsProvider: