/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ /* * Copyright 2011-2020 Couchbase, Inc. * * Licensed 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. */ #include "config.h" #include "iotests.h" #include #define DESIGN_DOC_NAME "lcb_design_doc" #define VIEW_NAME "lcb-test-view" class HttpUnitTest : public MockUnitTest { }; class HttpCmdContext { public: HttpCmdContext() : received(false), dumpIfEmpty(false), dumpIfError(false), cbCount(0) {} bool received; bool dumpIfEmpty; bool dumpIfError; unsigned cbCount; uint16_t status; lcb_STATUS err; std::string body; }; static const char *view_common = "{ " " \"id\" : \"_design/" DESIGN_DOC_NAME "\"," " \"language\" : \"javascript\"," " \"views\" : { " " \"" VIEW_NAME "\" : {" "\"map\":" " \"function(doc) { " "if (doc.testid == 'lcb') { emit(doc.id) } " " } \" " " } " "}" "}"; static const char *content_type = "application/json"; static void dumpResponse(const lcb_RESPHTTP *resp) { const char *const *headers; lcb_resphttp_headers(resp, &headers); if (headers) { for (const char *const *cur = headers; *cur; cur += 2) { std::cout << cur[0] << ": " << cur[1] << std::endl; } } const char *body; size_t nbody; lcb_resphttp_body(resp, &body, &nbody); if (body) { std::cout << "Data: " << std::endl; std::cout.write((const char *)body, nbody); std::cout << std::endl; } const char *path; size_t npath; lcb_resphttp_path(resp, &path, &npath); std::cout << "Path: " << std::endl; std::cout.write(path, npath); std::cout << std::endl; } extern "C" { static void httpSimpleCallback(lcb_INSTANCE *, lcb_CALLBACK_TYPE, const lcb_RESPHTTP *resp) { HttpCmdContext *htctx; lcb_resphttp_cookie(resp, (void **)&htctx); lcb_STATUS rc = lcb_resphttp_status(resp); htctx->err = rc; lcb_resphttp_http_status(resp, &htctx->status); htctx->received = true; htctx->cbCount++; const char *body; size_t nbody; lcb_resphttp_body(resp, &body, &nbody); if (body) { htctx->body.assign(body, nbody); } if ((nbody == 0 && htctx->dumpIfEmpty) || (rc != LCB_SUCCESS && htctx->dumpIfError)) { std::cout << "Count: " << htctx->cbCount << std::endl << "Code: " << rc << std::endl << "nBytes: " << nbody << std::endl; dumpResponse(resp); } } } /** * @test HTTP (Put) * * @pre Create a valid view document and store it on the server * @post Store succeeds and the HTTP result code is 201 */ TEST_F(HttpUnitTest, testPut) { SKIP_IF_MOCK(); HandleWrap hw; lcb_INSTANCE *instance; createConnection(hw, &instance); lcb_install_callback(instance, LCB_CALLBACK_HTTP, (lcb_RESPCALLBACK)httpSimpleCallback); std::string design_doc_path("/_design/" DESIGN_DOC_NAME); lcb_CMDHTTP *cmd; lcb_cmdhttp_create(&cmd, LCB_HTTP_TYPE_VIEW); lcb_cmdhttp_path(cmd, design_doc_path.c_str(), design_doc_path.size()); lcb_cmdhttp_method(cmd, LCB_HTTP_METHOD_PUT); lcb_cmdhttp_body(cmd, view_common, strlen(view_common)); lcb_cmdhttp_content_type(cmd, content_type, strlen(content_type)); lcb_HTTP_HANDLE *htreq; HttpCmdContext ctx; ctx.dumpIfError = true; lcb_cmdhttp_handle(cmd, &htreq); ASSERT_EQ(LCB_SUCCESS, lcb_http(instance, &ctx, cmd)); lcb_cmdhttp_destroy(cmd); lcb_wait(instance, LCB_WAIT_DEFAULT); ASSERT_EQ(true, ctx.received); ASSERT_EQ(LCB_SUCCESS, ctx.err); ASSERT_EQ(201, ctx.status); /* 201 Created */ ASSERT_EQ(1, ctx.cbCount); } /** * @test HTTP (Get) * @pre Query a value view * @post HTTP Result is @c 200, and the view contents look like valid JSON * (i.e. the first non-whitespace char is a @c { and the last non-whitespace * char is a @c } */ TEST_F(HttpUnitTest, testGet) { SKIP_IF_MOCK(); HandleWrap hw; lcb_INSTANCE *instance; createConnection(hw, &instance); lcb_install_callback(instance, LCB_CALLBACK_HTTP, (lcb_RESPCALLBACK)httpSimpleCallback); std::string view_path("/_design/" DESIGN_DOC_NAME "/_view/" VIEW_NAME); lcb_CMDHTTP *cmd; lcb_cmdhttp_create(&cmd, LCB_HTTP_TYPE_VIEW); lcb_cmdhttp_path(cmd, view_path.c_str(), view_path.size()); lcb_cmdhttp_method(cmd, LCB_HTTP_METHOD_GET); lcb_cmdhttp_content_type(cmd, content_type, strlen(content_type)); lcb_HTTP_HANDLE *htreq; HttpCmdContext ctx; ctx.dumpIfEmpty = true; ctx.dumpIfError = true; lcb_cmdhttp_handle(cmd, &htreq); ASSERT_EQ(LCB_SUCCESS, lcb_http(instance, &ctx, cmd)); lcb_cmdhttp_destroy(cmd); lcb_wait(instance, LCB_WAIT_DEFAULT); ASSERT_EQ(true, ctx.received); ASSERT_EQ(200, ctx.status); ASSERT_GT(ctx.body.size(), 0U); ASSERT_EQ(ctx.cbCount, 1); unsigned ii; const char *pcur; for (ii = 0, pcur = ctx.body.c_str(); ii < ctx.body.size() && isspace(*pcur); ii++, pcur++) { /* no body */ } /** * This is a view request. If all is in order, the content should be a * JSON object, first non-ws char is "{" and last non-ws char is "}" */ ASSERT_NE(ctx.body.size(), ii); ASSERT_EQ(*pcur, '{'); for (pcur = ctx.body.c_str() + ctx.body.size() - 1; ii >= 0 && isspace(*pcur); ii--, pcur--) { /* no body */ } ASSERT_GE(ii, 0U); ASSERT_EQ('}', *pcur); } /** * @test HTTP (Connection Refused) * @bug CCBC-132 * @pre Create a request of type RAW to @c localhost:1 - nothing should be * listening there * @post Command returns. Status code is one of CONNECT_ERROR or NETWORK_ERROR */ TEST_F(HttpUnitTest, testRefused) { lcb_INSTANCE *instance; HandleWrap hw; createConnection(hw, &instance); lcb_install_callback(instance, LCB_CALLBACK_HTTP, (lcb_RESPCALLBACK)httpSimpleCallback); std::string path("non-exist-path"); lcb_CMDHTTP *cmd; lcb_cmdhttp_create(&cmd, LCB_HTTP_TYPE_RAW); lcb_cmdhttp_path(cmd, path.c_str(), path.size()); const char *host = "localhost:1"; // should not have anything listening on it lcb_cmdhttp_host(cmd, host, strlen(host)); lcb_cmdhttp_method(cmd, LCB_HTTP_METHOD_GET); lcb_cmdhttp_content_type(cmd, content_type, strlen(content_type)); HttpCmdContext ctx; ctx.dumpIfEmpty = false; ctx.dumpIfError = false; lcb_HTTP_HANDLE *htreq; lcb_cmdhttp_handle(cmd, &htreq); ASSERT_EQ(LCB_SUCCESS, lcb_http(instance, &ctx, cmd)); lcb_cmdhttp_destroy(cmd); lcb_wait(instance, LCB_WAIT_DEFAULT); ASSERT_EQ(true, ctx.received); ASSERT_NE(0, LCB_ERROR_IS_NETWORK(ctx.err)); } struct HtResult { std::string body; std::map< std::string, std::string > headers; bool gotComplete; bool gotChunked; lcb_STATUS rc; uint16_t http_status; void reset() { body.clear(); gotComplete = false; gotChunked = false; rc = LCB_SUCCESS; http_status = 0; } }; extern "C" { static void http_callback(lcb_INSTANCE *, int, const lcb_RESPHTTP *resp) { HtResult *me; lcb_resphttp_cookie(resp, (void **)&me); me->rc = lcb_resphttp_status(resp); lcb_resphttp_http_status(resp, &me->http_status); const char *body; size_t nbody; lcb_resphttp_body(resp, &body, &nbody); if (nbody) { me->body.append(body, body + nbody); } if (lcb_resphttp_is_final(resp)) { me->gotComplete = true; const char *const *cur = NULL; lcb_resphttp_headers(resp, &cur); for (; *cur; cur += 2) { me->headers[cur[0]] = cur[1]; } } else { me->gotChunked = true; } } } static void makeAdminReq(lcb_CMDHTTP **cmd, std::string &bkbuf, lcb_INSTANCE *instance) { char *bucketname = NULL; lcb_cntl(instance, LCB_CNTL_GET, LCB_CNTL_BUCKETNAME, &bucketname); ASSERT_NE((const char *)NULL, bucketname); bkbuf.assign("/pools/default/buckets/"); bkbuf.append(bucketname); lcb_cmdhttp_create(cmd, LCB_HTTP_TYPE_MANAGEMENT); lcb_cmdhttp_method(*cmd, LCB_HTTP_METHOD_GET); lcb_cmdhttp_path(*cmd, bkbuf.c_str(), bkbuf.size()); } // Some more basic HTTP tests for the administrative API. We use the admin // API since it's always available. TEST_F(HttpUnitTest, testAdminApi) { lcb_INSTANCE *instance; HandleWrap hw; std::string pth; createConnection(hw, &instance); lcb_install_callback(instance, LCB_CALLBACK_HTTP, (lcb_RESPCALLBACK)http_callback); // Make the request; this time we make it to the 'management' API lcb_CMDHTTP *cmd = NULL; makeAdminReq(&cmd, pth, instance); HtResult htr; htr.reset(); lcb_STATUS err; lcb_sched_enter(instance); err = lcb_http(instance, &htr, cmd); ASSERT_EQ(LCB_SUCCESS, err); lcb_sched_leave(instance); lcb_wait(instance, LCB_WAIT_DEFAULT); ASSERT_TRUE(htr.gotComplete); ASSERT_EQ(LCB_SUCCESS, htr.rc); ASSERT_EQ(200, htr.http_status); ASSERT_FALSE(htr.body.empty()); // Try with a chunked request htr.reset(); lcb_cmdhttp_streaming(cmd, true); lcb_sched_enter(instance); err = lcb_http(instance, &htr, cmd); ASSERT_EQ(LCB_SUCCESS, err); lcb_sched_leave(instance); lcb_wait(instance, LCB_WAIT_DEFAULT); ASSERT_TRUE(htr.gotComplete); ASSERT_TRUE(htr.gotChunked); // try another one, but this time cancelling it.. lcb_HTTP_HANDLE *reqh; lcb_cmdhttp_handle(cmd, &reqh); lcb_sched_enter(instance); err = lcb_http(instance, NULL, cmd); ASSERT_EQ(LCB_SUCCESS, err); ASSERT_FALSE(reqh == NULL); lcb_sched_leave(instance); lcb_http_cancel(instance, reqh); // Try another one, allocating a request body. Unfortunately, we need // to cancel this one too, as none of the mock's endpoints support a // request body lcb_cmdhttp_handle(cmd, &reqh); lcb_cmdhttp_body(cmd, "FOO", 3); lcb_cmdhttp_method(cmd, LCB_HTTP_METHOD_PUT); err = lcb_http(instance, NULL, cmd); ASSERT_EQ(LCB_SUCCESS, err); ASSERT_FALSE(reqh == NULL); lcb_sched_leave(instance); lcb_http_cancel(instance, reqh); lcb_cmdhttp_destroy(cmd); } extern "C" { static void doubleCancel_callback(lcb_INSTANCE *instance, int, const lcb_RESPHTTP *resp) { if (lcb_resphttp_is_final(resp)) { lcb_HTTP_HANDLE *handle = NULL; lcb_resphttp_handle(resp, &handle); lcb_http_cancel(instance, handle); lcb_http_cancel(instance, handle); } } } TEST_F(HttpUnitTest, testDoubleCancel) { lcb_INSTANCE *instance; HandleWrap hw; createConnection(hw, &instance); lcb_install_callback(instance, LCB_CALLBACK_HTTP, (lcb_RESPCALLBACK)doubleCancel_callback); // Make the request; this time we make it to the 'management' API lcb_CMDHTTP *cmd = NULL; std::string bk; makeAdminReq(&cmd, bk, instance); lcb_sched_enter(instance); ASSERT_EQ(LCB_SUCCESS, lcb_http(instance, NULL, cmd)); lcb_cmdhttp_destroy(cmd); lcb_sched_leave(instance); lcb_wait(instance, LCB_WAIT_DEFAULT); // No crashes or errors here means we've done OK } extern "C" { static void cancelVerify_callback(lcb_INSTANCE *instance, int, const lcb_RESPHTTP *resp) { bool *bCancelled; lcb_resphttp_cookie(resp, (void **)&bCancelled); ASSERT_EQ(0, lcb_resphttp_is_final(resp)); ASSERT_FALSE(*bCancelled); lcb_HTTP_HANDLE *handle; lcb_resphttp_handle(resp, &handle); lcb_http_cancel(instance, handle); *bCancelled = true; } } // Ensure cancel actually does what it claims to do TEST_F(HttpUnitTest, testCancelWorks) { lcb_INSTANCE *instance; HandleWrap hw; createConnection(hw, &instance); lcb_install_callback(instance, LCB_CALLBACK_HTTP, (lcb_RESPCALLBACK)cancelVerify_callback); lcb_CMDHTTP *cmd = NULL; std::string ss; makeAdminReq(&cmd, ss, instance); // Make it chunked lcb_cmdhttp_streaming(cmd, true); bool cookie = false; lcb_sched_enter(instance); ASSERT_EQ(LCB_SUCCESS, lcb_http(instance, &cookie, cmd)); lcb_cmdhttp_destroy(cmd); lcb_sched_leave(instance); lcb_wait(instance, LCB_WAIT_DEFAULT); } extern "C" { static void noInvoke_callback(lcb_INSTANCE *, int, const lcb_RESPBASE *) { EXPECT_FALSE(true) << "This callback should not be invoked!"; } } TEST_F(HttpUnitTest, testDestroyWithActiveRequest) { lcb_INSTANCE *instance; // Note the one-arg form of createConnection which doesn't come with the // magical HandleWrap; this is because we destroy our instance explicitly // here. createConnection(&instance); lcb_CMDHTTP *cmd; std::string ss; makeAdminReq(&cmd, ss, instance); lcb_install_callback(instance, LCB_CALLBACK_HTTP, noInvoke_callback); lcb_sched_enter(instance); ASSERT_EQ(LCB_SUCCESS, lcb_http(instance, NULL, cmd)); lcb_cmdhttp_destroy(cmd); lcb_sched_leave(instance); lcb_destroy(instance); }