1/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2/*
3 *     Copyright 2015 Couchbase, Inc
4 *
5 *   Licensed under the Apache License, Version 2.0 (the "License");
6 *   you may not use this file except in compliance with the License.
7 *   You may obtain a copy of the License at
8 *
9 *       http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *   Unless required by applicable law or agreed to in writing, software
12 *   distributed under the License is distributed on an "AS IS" BASIS,
13 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *   See the License for the specific language governing permissions and
15 *   limitations under the License.
16 */
17
18/*
19 * Sub-document API multi-path tests
20 */
21
22#include "testapp_subdoc.h"
23
24#include "utilities/subdoc_encoder.h"
25
26// Test multi-path lookup command - simple single SUBDOC_GET
27TEST_P(McdTestappTest, SubdocMultiLookup_GetSingle)
28{
29    store_object("dict", "{\"key1\":1,\"key2\":\"two\", \"key3\":3.0}");
30
31    SubdocMultiLookupCmd lookup;
32    lookup.key = "dict";
33    lookup.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_GET, SUBDOC_FLAG_NONE,
34                            "key1"});
35    std::vector<SubdocMultiLookupResult> expected{{PROTOCOL_BINARY_RESPONSE_SUCCESS, "1"}};
36    expect_subdoc_cmd(lookup, PROTOCOL_BINARY_RESPONSE_SUCCESS, expected);
37
38    // Attempt to access non-existent key.
39    lookup.key = "dictXXX";
40    expected.clear();
41    expect_subdoc_cmd(lookup, PROTOCOL_BINARY_RESPONSE_KEY_ENOENT, expected);
42
43    // Attempt to access non-existent path.
44    lookup.key = "dict";
45    lookup.specs.at(0) = {PROTOCOL_BINARY_CMD_SUBDOC_GET, SUBDOC_FLAG_NONE,
46                          "keyXX"};
47    expected.push_back({PROTOCOL_BINARY_RESPONSE_SUBDOC_PATH_ENOENT, ""});
48    expect_subdoc_cmd(lookup, PROTOCOL_BINARY_RESPONSE_SUBDOC_MULTI_PATH_FAILURE,
49                      expected);
50
51    delete_object("dict");
52}
53
54// Test multi-path lookup command - simple single SUBDOC_EXISTS
55TEST_P(McdTestappTest, SubdocMultiLookup_ExistsSingle)
56{
57    store_object("dict", "{\"key1\":1,\"key2\":\"two\", \"key3\":3.0}");
58
59    SubdocMultiLookupCmd lookup;
60    lookup.key = "dict";
61    lookup.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_EXISTS,
62                            SUBDOC_FLAG_NONE, "key1"});
63    std::vector<SubdocMultiLookupResult> expected{{PROTOCOL_BINARY_RESPONSE_SUCCESS, ""}};
64    expect_subdoc_cmd(lookup, PROTOCOL_BINARY_RESPONSE_SUCCESS, expected);
65
66    delete_object("dict");
67}
68
69/* Creates a flat dictionary with the specified number of key/value pairs
70 *   Keys are named "key_0", "key_1"...
71 *   Values are strings of the form "value_0", value_1"...
72 */
73unique_cJSON_ptr make_flat_dict(int nelements) {
74    cJSON* dict = cJSON_CreateObject();
75    for (int i = 0; i < nelements; i++) {
76        std::string key("key_" + std::to_string(i));
77        std::string value("value_" + std::to_string(i));
78        cJSON_AddStringToObject(dict, key.c_str(), value.c_str());
79    }
80    return unique_cJSON_ptr(dict);
81}
82
83static void test_subdoc_multi_lookup_getmulti() {
84    auto dict = make_flat_dict(PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS + 1);
85    auto* dict_str = cJSON_Print(dict.get());
86    store_object("dict", dict_str);
87    cJSON_Free(dict_str);
88
89    // Lookup the maximum number of allowed paths - should succeed.
90    SubdocMultiLookupCmd lookup;
91    lookup.key = "dict";
92    std::vector<SubdocMultiLookupResult> expected;
93    for (int ii = 0; ii < PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS; ii++)
94    {
95        std::string key("key_" + std::to_string(ii));
96        std::string value("\"value_" + std::to_string(ii) + '"');
97        lookup.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_GET,
98                                SUBDOC_FLAG_NONE, key});
99
100        expected.push_back({PROTOCOL_BINARY_RESPONSE_SUCCESS,
101                            value});
102    }
103    expect_subdoc_cmd(lookup, PROTOCOL_BINARY_RESPONSE_SUCCESS, expected);
104
105    // Add one more - should fail.
106    lookup.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_GET, SUBDOC_FLAG_NONE,
107                            "key_" + std::to_string(PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS)});
108    expected.clear();
109    expect_subdoc_cmd(lookup, PROTOCOL_BINARY_RESPONSE_SUBDOC_INVALID_COMBO,
110                      expected);
111    reconnect_to_server();
112
113    delete_object("dict");
114}
115// Test multi-path lookup - multiple GET lookups
116TEST_P(McdTestappTest, SubdocMultiLookup_GetMulti) {
117    test_subdoc_multi_lookup_getmulti();
118}
119
120// Test multi-path lookup - multiple GET lookups with various invalid paths.
121TEST_P(McdTestappTest, SubdocMultiLookup_GetMultiInvalid)
122{
123    store_object("dict", "{\"key1\":1,\"key2\":\"two\",\"key3\":[0,1,2]}");
124
125    // Build a multi-path LOOKUP with a variety of invalid paths
126    std::vector<std::pair<std::string, protocol_binary_response_status > > bad_paths({
127            {"[0]", PROTOCOL_BINARY_RESPONSE_SUBDOC_PATH_MISMATCH},
128            {"key3[3]", PROTOCOL_BINARY_RESPONSE_SUBDOC_PATH_ENOENT},
129    });
130
131    SubdocMultiLookupCmd lookup;
132    lookup.key = "dict";
133    std::vector<SubdocMultiLookupResult> expected;
134    for (const auto& path : bad_paths) {
135        lookup.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_GET,
136                                SUBDOC_FLAG_NONE, path.first});
137        expected.push_back({path.second, ""});
138    }
139    expect_subdoc_cmd(lookup,
140                      PROTOCOL_BINARY_RESPONSE_SUBDOC_MULTI_PATH_FAILURE,
141                      expected);
142
143    delete_object("dict");
144}
145
146// Test multi-path lookup - multiple EXISTS lookups
147TEST_P(McdTestappTest, SubdocMultiLookup_ExistsMulti)
148{
149    auto dict = make_flat_dict(PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS + 1);
150    auto* dict_str = cJSON_Print(dict.get());
151    store_object("dict", dict_str);
152    cJSON_Free(dict_str);
153
154    // Lookup the maximum number of allowed paths - should succeed.
155    SubdocMultiLookupCmd lookup;
156    lookup.key = "dict";
157    std::vector<SubdocMultiLookupResult> expected;
158    for (int ii = 0; ii < PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS; ii++) {
159        std::string key("key_" + std::to_string(ii));
160
161        lookup.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_EXISTS,
162                                SUBDOC_FLAG_NONE, key });
163        expected.push_back({PROTOCOL_BINARY_RESPONSE_SUCCESS, ""});
164    }
165    expect_subdoc_cmd(lookup, PROTOCOL_BINARY_RESPONSE_SUCCESS, expected);
166
167    // Add one more - should fail.
168    lookup.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_EXISTS,
169                            SUBDOC_FLAG_NONE,
170                            "key_" + std::to_string(PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS)});
171    expected.clear();
172    expect_subdoc_cmd(lookup, PROTOCOL_BINARY_RESPONSE_SUBDOC_INVALID_COMBO,
173                      expected);
174    reconnect_to_server();
175
176    delete_object("dict");
177}
178
179/******************* Multi-path mutation tests *******************************/
180
181// Test multi-path mutation command - simple single SUBDOC_DICT_ADD
182TEST_P(McdTestappTest, SubdocMultiMutation_DictAddSingle)
183{
184    store_object("dict", "{}");
185
186    SubdocMultiMutationCmd mutation;
187    mutation.key = "dict";
188    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
189                              SUBDOC_FLAG_NONE, "key", "\"value\""});
190    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
191
192    // Check the update actually occurred.
193    validate_object("dict", "{\"key\":\"value\"}");
194
195    delete_object("dict");
196}
197
198// Test multi-path mutation command - simple multiple SUBDOC_DICT_ADD
199TEST_P(McdTestappTest, SubdocMultiMutation_DictAddMulti)
200{
201    store_object("dict", "{}");
202
203    SubdocMultiMutationCmd mutation;
204    mutation.key = "dict";
205    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
206                              SUBDOC_FLAG_NONE, "key1", "1"});
207    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
208                              SUBDOC_FLAG_NONE, "key2", "2"});
209    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
210                              SUBDOC_FLAG_NONE, "key3", "3"});
211    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
212
213    // Check the update actually occurred.
214    validate_object("dict", "{\"key1\":1,\"key2\":2,\"key3\":3}");
215
216    delete_object("dict");
217}
218
219// Test multi-path mutation command - test maximum supported SUBDOC_DICT_ADD
220// paths.
221static void test_subdoc_multi_mutation_dict_add_max() {
222    store_object("dict", "{}");
223
224    SubdocMultiMutationCmd mutation;
225    mutation.key = "dict";
226    for (int ii = 0; ii < PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS; ii++) {
227        std::string path("key_" + std::to_string(ii));
228        std::string value("\"value_" + std::to_string(ii) + '"');
229
230        mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
231                                  SUBDOC_FLAG_NONE, path, value});
232    }
233    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
234
235    // Check the update actually occurred.
236    auto dict = make_flat_dict(PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS);
237    auto* expected_str = cJSON_PrintUnformatted(dict.get());
238    validate_object("dict", expected_str);
239    cJSON_Free(expected_str);
240
241    delete_object("dict");
242
243    // Try with one more mutation spec - should fail and document should be
244    // unmodified.
245    store_object("dict", "{}");
246    auto max_id = std::to_string(PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS);
247    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
248                              SUBDOC_FLAG_NONE, "key_" + max_id,
249                              "\"value_" + max_id + '"'});
250    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUBDOC_INVALID_COMBO,
251                      {});
252
253    reconnect_to_server();
254
255    // Document should be unmodified.
256    validate_object("dict", "{}");
257
258    delete_object("dict");
259}
260TEST_P(McdTestappTest, SubdocMultiMutation_DictAddMax) {
261    test_subdoc_multi_mutation_dict_add_max();
262}
263
264// Test attempting to add the same key twice in a multi-path command.
265TEST_P(McdTestappTest, SubdocMultiMutation_DictAddInvalidDuplicate) {
266    store_object("dict", "{}");
267
268    SubdocMultiMutationCmd mutation;
269    mutation.key = "dict";
270    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
271                              SUBDOC_FLAG_NONE, "key", "\"value\""});
272    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
273                              SUBDOC_FLAG_NONE, "key", "\"value2\""});
274    // Should return failure, with the index of the failing op (1).
275    expect_subdoc_cmd(mutation,
276                      PROTOCOL_BINARY_RESPONSE_SUBDOC_MULTI_PATH_FAILURE,
277                      {{1, PROTOCOL_BINARY_RESPONSE_SUBDOC_PATH_EEXISTS}});
278
279    // Document should be unmodified.
280    validate_object("dict", "{}");
281
282    delete_object("dict");
283}
284
285// Test multi-path mutation command - 2x DictAdd with a Counter update
286TEST_P(McdTestappTest, SubdocMultiMutation_DictAddCounter) {
287    store_object("dict", "{\"count\":0,\"items\":{}}");
288
289    SubdocMultiMutationCmd mutation;
290    mutation.key = "dict";
291    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
292                              SUBDOC_FLAG_NONE, "items.foo", "1"});
293    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
294                              SUBDOC_FLAG_NONE, "items.bar", "2"});
295    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_COUNTER,
296                              SUBDOC_FLAG_NONE, "count", "2"});
297    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS,
298                      {{2, PROTOCOL_BINARY_RESPONSE_SUCCESS, "2"}});
299
300    // Check the update actually occurred.
301    validate_object("dict",
302                    "{\"count\":2,\"items\":{\"foo\":1,\"bar\":2}}");
303
304    delete_object("dict");
305}
306
307// Test multi-path mutation command - 2x DictAdd with specific CAS.
308TEST_P(McdTestappTest, SubdocMultiMutation_DictAddCAS) {
309    store_object("dict", "{\"int\":1}");
310
311    // Use SUBDOC_EXISTS to obtain the current CAS.
312    BinprotSubdocResponse resp;
313    subdoc_verify_cmd(
314        BinprotSubdocCommand(PROTOCOL_BINARY_CMD_SUBDOC_EXISTS).setKey("dict").setPath("int"),
315        PROTOCOL_BINARY_RESPONSE_SUCCESS, "", resp);
316    auto cas = resp.getCas();
317
318    // 1. Attempt to mutate with an incorrect CAS - should fail.
319    SubdocMultiMutationCmd mutation;
320    mutation.key = "dict";
321    mutation.cas = cas - 1;
322    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
323                              SUBDOC_FLAG_NONE, "float", "2.0"});
324    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
325                              SUBDOC_FLAG_NONE, "string", "\"value\""});
326    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_KEY_EEXISTS, {});
327    // Document should be unmodified.
328    validate_object("dict", "{\"int\":1}");
329
330    // 2. Attempt to mutate with correct CAS.
331    mutation.cas = cas;
332    uint64_t new_cas = expect_subdoc_cmd(mutation,
333                                         PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
334
335    // CAS should have changed.
336    EXPECT_NE(cas, new_cas);
337
338    // Document should have been updated.
339    validate_object("dict", "{\"int\":1,\"float\":2.0,\"string\":\"value\"}");
340
341    delete_object("dict");
342}
343
344// Test multi-path mutation command - create a bunch of dictionary elements
345// then delete them. (Not a very useful operation but should work).
346void test_subdoc_multi_mutation_dictadd_delete() {
347    store_object("dict", "{\"count\":0,\"items\":{}}");
348
349    // 1. Add a series of paths, then remove two of them.
350    SubdocMultiMutationCmd mutation;
351    mutation.key = "dict";
352    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
353                              SUBDOC_FLAG_NONE, "items.1", "1"});
354    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
355                              SUBDOC_FLAG_NONE, "items.2", "2"});
356    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
357                              SUBDOC_FLAG_NONE, "items.3", "3"});
358    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_COUNTER,
359                              SUBDOC_FLAG_NONE, "count", "3"});
360    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DELETE,
361                              SUBDOC_FLAG_NONE, "items.1"});
362    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DELETE,
363                              SUBDOC_FLAG_NONE, "items.3"});
364    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_COUNTER,
365                              SUBDOC_FLAG_NONE, "count", "-2"});
366    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS,
367                      {{3, PROTOCOL_BINARY_RESPONSE_SUCCESS, "3"},
368                       {6, PROTOCOL_BINARY_RESPONSE_SUCCESS, "1"}});
369
370    // Document should have been updated.
371    validate_object("dict", "{\"count\":1,\"items\":{\"2\":2}}");
372
373    // 2. Delete the old 'items' dictionary and create a new one.
374    mutation.specs.clear();
375    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DELETE,
376                              SUBDOC_FLAG_NONE, "items"});
377    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_UPSERT,
378                              SUBDOC_FLAG_NONE, "count", "0"});
379    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
380                              SUBDOC_FLAG_MKDIR_P, "items.4", "4"});
381    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
382                              SUBDOC_FLAG_NONE, "items.5", "5"});
383    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_COUNTER,
384                              SUBDOC_FLAG_NONE, "count", "2"});
385    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS,
386                      {{4, PROTOCOL_BINARY_RESPONSE_SUCCESS, "2"}});
387
388    validate_object("dict", "{\"count\":2,\"items\":{\"4\":4,\"5\":5}}");
389
390    delete_object("dict");
391}
392
393TEST_P(McdTestappTest, SubdocMultiMutation_DictAddDelete) {
394    test_subdoc_multi_mutation_dictadd_delete();
395}
396
397TEST_P(McdTestappTest, SubdocMultiMutation_DictAddDelete_MutationSeqno) {
398    set_mutation_seqno_feature(true);
399    test_subdoc_multi_mutation_dictadd_delete();
400    set_mutation_seqno_feature(false);
401}
402
403// Test support for expiration on multi-path commands.
404TEST_P(McdTestappTest, SubdocMultiMutation_Expiry) {
405    // Create two documents; one to be used for an exlicit 1s expiry and one
406    // for an explicit 0s (i.e. never) expiry.
407    store_object("ephemeral", "[\"a\"]");
408    store_object("permanent", "[\"a\"]");
409
410    // Expiry not permitted for MULTI_LOOKUP operations.
411    SubdocMultiLookupCmd lookup;
412    lookup.key = "ephemeral";
413    lookup.expiry = 666;
414    lookup.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_EXISTS,
415                            SUBDOC_FLAG_NONE, "[0]" });
416    expect_subdoc_cmd(lookup, PROTOCOL_BINARY_RESPONSE_EINVAL, {});
417    reconnect_to_server();
418
419    // Perform a MULTI_REPLACE operation, setting a expiry of 1s.
420    SubdocMultiMutationCmd mutation;
421    mutation.key = "ephemeral";
422    mutation.expiry = 1;
423    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_REPLACE,
424                              SUBDOC_FLAG_NONE, "[0]", "\"b\""});
425    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
426
427    // Try to read the document immediately - should exist.
428    auto result = fetch_value("ephemeral");
429    EXPECT_EQ(PROTOCOL_BINARY_RESPONSE_SUCCESS, result.first);
430    EXPECT_EQ("[\"b\"]", result.second);
431
432    // Perform a REPLACE on permanent, explicitly encoding an expiry of 0s.
433    mutation.key = "permanent";
434    mutation.expiry = 0;
435    mutation.encode_zero_expiry_on_wire = true;
436    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
437
438    // Try to read the second document immediately - should exist.
439    result = fetch_value("permanent");
440    EXPECT_EQ(PROTOCOL_BINARY_RESPONSE_SUCCESS, result.first);
441    EXPECT_EQ("[\"b\"]", result.second);
442
443    // Sleep for 2s seconds.
444    // TODO: it would be great if we could somehow accelerate time from the
445    // harness, and not add 2s to the runtime of the test...
446    usleep(2 * 1000 * 1000);
447
448    // Try to read the ephemeral document - shouldn't exist.
449    result = fetch_value("ephemeral");
450    EXPECT_EQ(PROTOCOL_BINARY_RESPONSE_KEY_ENOENT, result.first);
451
452    // Try to read the permanent document - should still exist.
453    result = fetch_value("permanent");
454    EXPECT_EQ(PROTOCOL_BINARY_RESPONSE_SUCCESS, result.first);
455    EXPECT_EQ("[\"b\"]", result.second);
456}
457
458// Test statistics support for multi-lookup commands
459TEST_P(McdTestappTest, SubdocStatsMultiLookup) {
460    // A multi-lookup counts as a single operation, irrespective of how many
461    // path specs it contains.
462
463    // Get initial stats
464    auto stats = request_stats();
465    auto count_before = extract_single_stat(stats, "cmd_subdoc_lookup");
466    auto bytes_before_total = extract_single_stat(stats, "bytes_subdoc_lookup_total");
467    auto bytes_before_subset = extract_single_stat(stats, "bytes_subdoc_lookup_extracted");
468
469    // Perform a multi-lookup containing >1 path.
470    test_subdoc_multi_lookup_getmulti();
471
472    // Get subsequent stats, check stat increased by one.
473    stats = request_stats();
474    auto count_after = extract_single_stat(stats, "cmd_subdoc_lookup");
475    auto bytes_after_total = extract_single_stat(stats, "bytes_subdoc_lookup_total");
476    auto bytes_after_subset = extract_single_stat(stats, "bytes_subdoc_lookup_extracted");
477    EXPECT_EQ(1, count_after - count_before);
478    EXPECT_EQ(373, bytes_after_total - bytes_before_total);
479    EXPECT_EQ(246, bytes_after_subset - bytes_before_subset);
480}
481
482// Test statistics support for multi-mutation commands
483TEST_P(McdTestappTest, SubdocStatsMultiMutation) {
484    // A multi-mutation counts as a single operation, irrespective of how many
485    // path specs it contains.
486
487    // Get initial stats
488    auto stats = request_stats();
489    auto count_before = extract_single_stat(stats, "cmd_subdoc_mutation");
490    auto bytes_before_total = extract_single_stat(stats, "bytes_subdoc_mutation_total");
491    auto bytes_before_subset = extract_single_stat(stats, "bytes_subdoc_mutation_inserted");
492
493    // Perform a multi-mutation containing >1 path.
494    test_subdoc_multi_mutation_dict_add_max();
495
496    // Get subsequent stats, check stat increased by one.
497    stats = request_stats();
498    auto count_after = extract_single_stat(stats, "cmd_subdoc_mutation");
499    auto bytes_after_total = extract_single_stat(stats, "bytes_subdoc_mutation_total");
500    auto bytes_after_subset = extract_single_stat(stats, "bytes_subdoc_mutation_inserted");
501    EXPECT_EQ(count_before + 1, count_after);
502    EXPECT_EQ(301, bytes_after_total - bytes_before_total);
503    EXPECT_EQ(150, bytes_after_subset - bytes_before_subset);
504}
505
506// Test support for multi-mutations returning values - maximum spec count
507TEST_P(McdTestappTest, SubdocMultiMutation_MaxResultSpecValue) {
508    // Create an array of PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS counters.
509    std::string input("[");
510    std::string expected_json("[");
511    for (int ii = 0; ii < PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS; ii++) {
512        input += "0,";
513        expected_json += std::to_string(ii + 1) + ",";
514    }
515    input.pop_back();
516    input += ']';
517    store_object("array", input.c_str());
518    expected_json.pop_back();
519    expected_json += ']';
520
521    SubdocMultiMutationCmd mutation;
522    mutation.key = "array";
523    std::vector<SubdocMultiMutationResult> expected_results;
524    for (uint8_t ii = 0; ii < PROTOCOL_BINARY_SUBDOC_MULTI_MAX_PATHS; ii++) {
525        std::string value("[" + std::to_string(ii) + "]");
526        mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_COUNTER,
527            SUBDOC_FLAG_NONE, value,
528            std::to_string(ii + 1)});
529
530        expected_results.push_back({ii, PROTOCOL_BINARY_RESPONSE_SUCCESS,
531            std::to_string(ii + 1)});
532    }
533
534    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS,
535                      expected_results);
536
537    validate_object("array", expected_json);
538
539    delete_object("array");
540
541}
542
543// Test that flags are preserved by subdoc multipath mutation operations.
544TEST_P(McdTestappTest, SubdocMultiMutation_Flags)
545{
546    const uint32_t flags = 0xcafebabe;
547    store_object_with_flags("array", "[]", flags);
548
549    SubdocMultiMutationCmd mutation;
550    mutation.key = "array";
551    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_ARRAY_PUSH_LAST,
552                              SUBDOC_FLAG_NONE, "", "0"});
553    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
554
555    // Check the update actually occurred.
556    validate_object("array", "[0]");
557    validate_flags("array", 0xcafebabe);
558
559    delete_object("array");
560}
561
562// Test that you can create a document with the Add doc flag
563TEST_P(McdTestappTest, SubdocMultiMutation_AddDocFlag) {
564    SubdocMultiMutationCmd mutation;
565    mutation.addDocFlag(mcbp::subdoc::doc_flag::Add);
566    mutation.key = "AddDocTest";
567    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_UPSERT,
568                              SUBDOC_FLAG_NONE,
569                              "test",
570                              "56"});
571    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
572    validate_object("AddDocTest", "{\"test\":56}");
573
574    delete_object("AddDocTest");
575}
576
577// Test that a command with an Add doc flag fails if the key exists
578TEST_P(McdTestappTest, SubdocMultiMutation_AddDocFlagEEXists) {
579    store_object("AddDocExistsTest", "[1,2,3,4]");
580
581    SubdocMultiMutationCmd mutation;
582    mutation.addDocFlag(mcbp::subdoc::doc_flag::Add);
583    mutation.key = "AddDocExistsTest";
584    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_UPSERT,
585                              SUBDOC_FLAG_NONE,
586                              "test",
587                              "56"});
588    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_KEY_EEXISTS, {});
589
590    delete_object("AddDocExistsTest");
591
592    // Now the doc is deleted, we should be able to Add successfully
593    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
594    validate_object("AddDocExistsTest", "{\"test\":56}");
595
596    delete_object("AddDocExistsTest");
597}
598
599// An Addd doesn't make sense with a cas, check that it's rejected
600TEST_P(McdTestappTest, SubdocMultiMutation_AddDocFlagInavlidCas) {
601    SubdocMultiMutationCmd mutation;
602    mutation.addDocFlag(mcbp::subdoc::doc_flag::Add);
603    mutation.key = "AddDocCas";
604    mutation.cas = 123456;
605    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_UPSERT,
606                              SUBDOC_FLAG_NONE,
607                              "test",
608                              "56"});
609    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_EINVAL, {});
610}
611
612// MB-30278: Perform a multi-mutation with two paths with backticks in them.
613TEST_P(McdTestappTest, MB_30278_SubdocBacktickMultiMutation) {
614    store_object("dict", "{}");
615
616    SubdocMultiMutationCmd mutation;
617    mutation.key = "dict";
618    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
619                              SUBDOC_FLAG_NONE,
620                              "key1``",
621                              "1"});
622    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
623                              SUBDOC_FLAG_NONE,
624                              "key2``",
625                              "2"});
626    mutation.specs.push_back({PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD,
627                              SUBDOC_FLAG_NONE,
628                              "key3``",
629                              "3"});
630    expect_subdoc_cmd(mutation, PROTOCOL_BINARY_RESPONSE_SUCCESS, {});
631
632    validate_object("dict", R"({"key1`":1,"key2`":2,"key3`":3})");
633
634    delete_object("dict");
635}
636