1/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2/*
3 *     Copyright 2017 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#include <cctype>
19#include <limits>
20#include <thread>
21#include "testapp_arithmetic.h"
22
23class ClusterConfigTest : public TestappXattrClientTest {
24protected:
25    void SetUp() override {
26        TestappXattrClientTest::SetUp();
27        // Make sure we've specified a session token
28        setClusterSessionToken(0xdeadbeef);
29    }
30
31    BinprotResponse setClusterConfig(uint64_t token,
32                                     const std::string& config) {
33        auto& conn = getAdminConnection();
34        conn.selectBucket("default");
35        BinprotResponse response;
36        conn.executeCommand(BinprotSetClusterConfigCommand{token, config},
37                            response);
38        return response;
39    }
40
41    void test_MB_17506(bool dedupe);
42};
43
44void ClusterConfigTest::test_MB_17506(bool dedupe) {
45    // First set the correct deduplication mode
46    auto* value = cJSON_GetObjectItem(memcached_cfg.get(), "dedupe_nmvb_maps");
47    ASSERT_NE(nullptr, value);
48    if (dedupe) {
49        cJSON_ReplaceItemInObject(
50                memcached_cfg.get(), "dedupe_nmvb_maps", cJSON_CreateTrue());
51    } else {
52        cJSON_ReplaceItemInObject(
53                memcached_cfg.get(), "dedupe_nmvb_maps", cJSON_CreateFalse());
54    }
55    reconfigure();
56
57    const std::string clustermap{R"({"rev":100})"};
58
59    // Make sure we have a cluster configuration installed
60    auto response = setClusterConfig(token, clustermap);
61    EXPECT_TRUE(response.isSuccess());
62
63    auto& conn = getConnection();
64    BinprotGetCommand command;
65    command.setKey("foo");
66    command.setVBucket(1);
67
68    // Execute the first get command. This one should _ALWAYS_ contain a map
69    conn.executeCommand(command, response);
70
71    ASSERT_FALSE(response.isSuccess());
72    ASSERT_EQ(PROTOCOL_BINARY_RESPONSE_NOT_MY_VBUCKET, response.getStatus());
73    EXPECT_EQ(clustermap, response.getDataString());
74
75    // Execute it one more time..
76    conn.executeCommand(command, response);
77
78    ASSERT_FALSE(response.isSuccess());
79    ASSERT_EQ(PROTOCOL_BINARY_RESPONSE_NOT_MY_VBUCKET, response.getStatus());
80
81    if (dedupe) {
82        EXPECT_TRUE(response.getDataString().empty())
83                << "Expected an empty stream, got [" << response.getDataString()
84                << "]";
85    } else {
86        EXPECT_EQ(clustermap, response.getDataString());
87    }
88}
89
90INSTANTIATE_TEST_CASE_P(
91        TransportProtocols,
92        ClusterConfigTest,
93        ::testing::Combine(::testing::Values(TransportProtocols::McbpPlain,
94                                             TransportProtocols::McbpIpv6Plain,
95                                             TransportProtocols::McbpSsl,
96                                             TransportProtocols::McbpIpv6Ssl),
97                           ::testing::Values(XattrSupport::Yes,
98                                             XattrSupport::No),
99                           ::testing::Values(ClientJSONSupport::Yes,
100                                             ClientJSONSupport::No),
101                           ::testing::Values(ClientSnappySupport::No)),
102        PrintToStringCombinedName());
103
104TEST_P(ClusterConfigTest, SetClusterConfigWithIncorrectSessionToken) {
105    auto response = setClusterConfig(0xcafebeef, R"({"rev":100})");
106    EXPECT_FALSE(response.isSuccess()) << "Should not be allowed to set "
107                                          "cluster config with invalid session "
108                                          "token";
109    EXPECT_EQ(PROTOCOL_BINARY_RESPONSE_KEY_EEXISTS, response.getStatus());
110}
111
112TEST_P(ClusterConfigTest, SetClusterConfigWithCorrectTokenInvalidPayload) {
113    auto response = setClusterConfig(token, R"({"foo":"bar"})");
114    EXPECT_FALSE(response.isSuccess())
115            << "Should not be allowed to set cluster config invalid payload";
116    EXPECT_EQ(PROTOCOL_BINARY_RESPONSE_EINVAL, response.getStatus());
117}
118
119TEST_P(ClusterConfigTest, SetClusterConfigWithCorrectToken) {
120    auto response = setClusterConfig(token, R"({"rev":100})");
121    EXPECT_TRUE(response.isSuccess()) << "Should be allowed to set cluster "
122                                         "config with the correct session "
123                                         "token";
124}
125
126TEST_P(ClusterConfigTest, GetClusterConfig) {
127    const std::string config{R"({"rev":100})"};
128    auto response = setClusterConfig(token, config);
129    ASSERT_TRUE(response.isSuccess());
130
131    BinprotGenericCommand cmd{PROTOCOL_BINARY_CMD_GET_CLUSTER_CONFIG, "", ""};
132    auto& conn = getConnection();
133    conn.executeCommand(cmd, response);
134    EXPECT_TRUE(response.isSuccess());
135    const auto value = response.getDataString();
136    EXPECT_EQ(config, value);
137    EXPECT_TRUE(hasCorrectDatatype(expectedJSONDatatype(),
138                                   cb::mcbp::Datatype(response.getDatatype()),
139                                   {value.data(), value.size()}));
140}
141
142TEST_P(ClusterConfigTest, test_MB_17506_no_dedupe) {
143    test_MB_17506(false);
144}
145
146TEST_P(ClusterConfigTest, test_MB_17506_dedupe) {
147    test_MB_17506(true);
148}
149
150TEST_P(ClusterConfigTest, Enable_CCCP_Push_Notifications) {
151    auto& conn = getConnection();
152    // The "connection class" ignore the context part in
153    // extended error message unless we enable the JSON datatype
154    conn.setDatatypeJson(true);
155
156    conn.setClustermapChangeNotification(false);
157    conn.setDuplexSupport(false);
158
159    try {
160        conn.setClustermapChangeNotification(true);
161        FAIL() << "It should not be possible to enable CCCP push notifications "
162                  "without duplex";
163    } catch (const std::runtime_error& e) {
164        EXPECT_STREQ(
165                "Failed to say hello: 'ClustermapChangeNotification needs "
166                "Duplex', Invalid arguments (4)",
167                e.what());
168    }
169
170    // With duplex we should we good to go
171    conn.setDuplexSupport(true);
172    conn.setClustermapChangeNotification(true);
173}
174
175TEST_P(ClusterConfigTest, CccpPushNotification) {
176    auto& conn = getAdminConnection();
177    conn.selectBucket("default");
178
179    auto second = conn.clone();
180
181    second->setDuplexSupport(true);
182    second->setClustermapChangeNotification(true);
183
184    BinprotResponse response;
185    conn.executeCommand(BinprotSetClusterConfigCommand{token, R"({"rev":666})"},
186                        response);
187
188    Frame frame;
189
190    // Setting a new config should cause the server to push a new config
191    // to me!
192    second->recvFrame(frame, false);
193    EXPECT_EQ(cb::mcbp::Magic::ServerRequest, frame.getMagic());
194
195    auto* request = frame.getRequest();
196
197    EXPECT_EQ(cb::mcbp::ServerOpcode::ClustermapChangeNotification,
198              request->getServerOpcode());
199    EXPECT_EQ(4, request->getExtlen());
200    auto extras = request->getExtdata();
201    uint32_t revno;
202    std::copy(extras.begin(), extras.end(), reinterpret_cast<uint8_t*>(&revno));
203    revno = ntohl(revno);
204    EXPECT_EQ(666, revno);
205
206    auto key = request->getKey();
207    const std::string bucket{reinterpret_cast<const char*>(key.data()),
208                             key.size()};
209    EXPECT_EQ("default", bucket);
210
211    auto value = request->getValue();
212    const std::string config{reinterpret_cast<const char*>(value.data()),
213                             value.size()};
214    EXPECT_EQ(R"({"rev":666})", config);
215}
216
217/**
218 * The bucket configuration was not reset as part of bucket deletion
219 */
220TEST_P(ClusterConfigTest, MB35395) {
221    setClusterConfig(token, R"({"rev":1000})");
222
223    auto& conn = getAdminConnection();
224    conn.deleteBucket("default");
225
226    // Recreate the bucket, and the cluster config should be gone!
227    CreateTestBucket();
228    conn = getAdminConnection();
229    conn.selectBucket("default");
230    BinprotGenericCommand cmd{PROTOCOL_BINARY_CMD_GET_CLUSTER_CONFIG, "",""};
231    BinprotResponse rsp;
232    conn.executeCommand(cmd, rsp);
233    ASSERT_EQ(PROTOCOL_BINARY_RESPONSE_KEY_ENOENT, rsp.getStatus());
234    EXPECT_EQ("", rsp.getDataString());
235}
236