Mercurial > hg > orthanc
diff OrthancFramework/UnitTestsSources/RestApiTests.cpp @ 4044:d25f4c0fa160 framework
splitting code into OrthancFramework and OrthancServer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 10 Jun 2020 20:30:34 +0200 |
parents | UnitTestsSources/RestApiTests.cpp@27628b0f6ada |
children | 05b8fd21089c |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp Wed Jun 10 20:30:34 2020 +0200 @@ -0,0 +1,871 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +# include <OrthancFramework.h> +#endif + +#include "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include <ctype.h> +#include <boost/lexical_cast.hpp> +#include <algorithm> + +#include "../Core/ChunkedBuffer.h" +#include "../Core/HttpClient.h" +#include "../Core/Logging.h" +#include "../Core/SystemToolbox.h" +#include "../Core/RestApi/RestApi.h" +#include "../Core/OrthancException.h" +#include "../Core/Compression/ZlibCompressor.h" +#include "../Core/RestApi/RestApiHierarchy.h" +#include "../Core/HttpServer/HttpContentNegociation.h" +#include "../Core/HttpServer/MultipartStreamReader.h" + + +using namespace Orthanc; + +#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS) +#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS" +#endif + + + +TEST(HttpClient, Basic) +{ + HttpClient c; + ASSERT_FALSE(c.IsVerbose()); + c.SetVerbose(true); + ASSERT_TRUE(c.IsVerbose()); + c.SetVerbose(false); + ASSERT_FALSE(c.IsVerbose()); + +#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 + // The "http://www.orthanc-server.com/downloads/third-party/" does + // not automatically redirect to HTTPS, so we cas use it even if the + // OpenSSL/HTTPS support is disabled in curl + const std::string BASE = "http://www.orthanc-server.com/downloads/third-party/"; + + Json::Value v; + c.SetUrl(BASE + "Product.json"); + + c.Apply(v); + ASSERT_TRUE(v.type() == Json::objectValue); + ASSERT_TRUE(v.isMember("Description")); +#endif +} + + +#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 && ORTHANC_ENABLE_SSL == 1 + +/** + The HTTPS CA certificates for BitBucket were extracted as follows: + + (1) We retrieve the certification chain of BitBucket: + + # echo | openssl s_client -showcerts -connect www.bitbucket.org:443 + + (2) We see that the certification authority (CA) is + "www.digicert.com", and the root certificate is "DigiCert High + Assurance EV Root CA". As a consequence, we navigate to DigiCert to + find the URL to this CA certificate: + + firefox https://www.digicert.com/digicert-root-certificates.htm + + (3) Once we get the URL to the CA certificate, we convert it to a C + macro that can be used by libcurl: + + # cd UnitTestsSources + # ../Resources/RetrieveCACertificates.py BITBUCKET_CERTIFICATES https://www.digicert.com/CACerts/DigiCertHighAssuranceEVRootCA.crt > BitbucketCACertificates.h +**/ + +#include "BitbucketCACertificates.h" + +TEST(HttpClient, Ssl) +{ + SystemToolbox::WriteFile(BITBUCKET_CERTIFICATES, "UnitTestsResults/bitbucket.cert"); + + /*{ + std::string s; + SystemToolbox::ReadFile(s, "/usr/share/ca-certificates/mozilla/WoSign.crt"); + SystemToolbox::WriteFile(s, "UnitTestsResults/bitbucket.cert"); + }*/ + + HttpClient c; + c.SetHttpsVerifyPeers(true); + c.SetHttpsCACertificates("UnitTestsResults/bitbucket.cert"); + + // Test file modified on 2020-04-20, in order to use a git + // repository on BitBucket instead of a Mercurial repository + // (because Mercurial support disappears on 2020-05-31) + c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json"); + + Json::Value v; + c.Apply(v); + ASSERT_TRUE(v.isMember("ServeFolders")); +} + +TEST(HttpClient, SslNoVerification) +{ + HttpClient c; + c.SetHttpsVerifyPeers(false); + c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json"); + + Json::Value v; + c.Apply(v); + ASSERT_TRUE(v.isMember("ServeFolders")); +} + +#endif + + +TEST(RestApi, ChunkedBuffer) +{ + ChunkedBuffer b; + ASSERT_EQ(0u, b.GetNumBytes()); + + b.AddChunk("hello", 5); + ASSERT_EQ(5u, b.GetNumBytes()); + + b.AddChunk("world", 5); + ASSERT_EQ(10u, b.GetNumBytes()); + + std::string s; + b.Flatten(s); + ASSERT_EQ("helloworld", s); +} + +TEST(RestApi, ParseCookies) +{ + IHttpHandler::Arguments headers; + IHttpHandler::Arguments cookies; + + headers["cookie"] = "a=b;c=d;;;e=f;;g=h;"; + HttpToolbox::ParseCookies(cookies, headers); + ASSERT_EQ(4u, cookies.size()); + ASSERT_EQ("b", cookies["a"]); + ASSERT_EQ("d", cookies["c"]); + ASSERT_EQ("f", cookies["e"]); + ASSERT_EQ("h", cookies["g"]); + + headers["cookie"] = " name = value ; name2=value2"; + HttpToolbox::ParseCookies(cookies, headers); + ASSERT_EQ(2u, cookies.size()); + ASSERT_EQ("value", cookies["name"]); + ASSERT_EQ("value2", cookies["name2"]); + + headers["cookie"] = " ;;; "; + HttpToolbox::ParseCookies(cookies, headers); + ASSERT_EQ(0u, cookies.size()); + + headers["cookie"] = " ; n=v ;; "; + HttpToolbox::ParseCookies(cookies, headers); + ASSERT_EQ(1u, cookies.size()); + ASSERT_EQ("v", cookies["n"]); +} + +TEST(RestApi, RestApiPath) +{ + IHttpHandler::Arguments args; + UriComponents trail; + + { + RestApiPath uri("/coucou/{abc}/d/*"); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/e/f/g")); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ(3u, trail.size()); + ASSERT_EQ("moi", args["abc"]); + ASSERT_EQ("e", trail[0]); + ASSERT_EQ("f", trail[1]); + ASSERT_EQ("g", trail[2]); + + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/f")); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/")); + ASSERT_FALSE(uri.Match(args, trail, "/a/moi/d")); + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi")); + + ASSERT_EQ(3u, uri.GetLevelCount()); + ASSERT_TRUE(uri.IsUniversalTrailing()); + + ASSERT_EQ("coucou", uri.GetLevelName(0)); + ASSERT_THROW(uri.GetWildcardName(0), OrthancException); + + ASSERT_EQ("abc", uri.GetWildcardName(1)); + ASSERT_THROW(uri.GetLevelName(1), OrthancException); + + ASSERT_EQ("d", uri.GetLevelName(2)); + ASSERT_THROW(uri.GetWildcardName(2), OrthancException); + } + + { + RestApiPath uri("/coucou/{abc}/d"); + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/d/e/f/g")); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d")); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ(0u, trail.size()); + ASSERT_EQ("moi", args["abc"]); + + ASSERT_EQ(3u, uri.GetLevelCount()); + ASSERT_FALSE(uri.IsUniversalTrailing()); + + ASSERT_EQ("coucou", uri.GetLevelName(0)); + ASSERT_THROW(uri.GetWildcardName(0), OrthancException); + + ASSERT_EQ("abc", uri.GetWildcardName(1)); + ASSERT_THROW(uri.GetLevelName(1), OrthancException); + + ASSERT_EQ("d", uri.GetLevelName(2)); + ASSERT_THROW(uri.GetWildcardName(2), OrthancException); + } + + { + RestApiPath uri("/*"); + ASSERT_TRUE(uri.Match(args, trail, "/a/b/c")); + ASSERT_EQ(0u, args.size()); + ASSERT_EQ(3u, trail.size()); + ASSERT_EQ("a", trail[0]); + ASSERT_EQ("b", trail[1]); + ASSERT_EQ("c", trail[2]); + + ASSERT_EQ(0u, uri.GetLevelCount()); + ASSERT_TRUE(uri.IsUniversalTrailing()); + } +} + + + + + + +static int testValue; + +template <int value> +static void SetValue(RestApiGetCall& get) +{ + testValue = value; +} + + +static bool GetDirectory(Json::Value& target, + RestApiHierarchy& hierarchy, + const std::string& uri) +{ + UriComponents p; + Toolbox::SplitUriComponents(p, uri); + return hierarchy.GetDirectory(target, p); +} + + + +namespace +{ + class MyVisitor : public RestApiHierarchy::IVisitor + { + public: + virtual bool Visit(const RestApiHierarchy::Resource& resource, + const UriComponents& uri, + const IHttpHandler::Arguments& components, + const UriComponents& trailing) ORTHANC_OVERRIDE + { + return resource.Handle(*(RestApiGetCall*) NULL); + } + }; +} + + +static bool HandleGet(RestApiHierarchy& hierarchy, + const std::string& uri) +{ + UriComponents p; + Toolbox::SplitUriComponents(p, uri); + MyVisitor visitor; + return hierarchy.LookupResource(p, visitor); +} + + +TEST(RestApi, RestApiHierarchy) +{ + RestApiHierarchy root; + root.Register("/hello/world/test", SetValue<1>); + root.Register("/hello/world/test2", SetValue<2>); + root.Register("/hello/{world}/test3/test4", SetValue<3>); + root.Register("/hello2/*", SetValue<4>); + + Json::Value m; + root.CreateSiteMap(m); + std::cout << m; + + Json::Value d; + ASSERT_FALSE(GetDirectory(d, root, "/hello")); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/a")); + ASSERT_EQ(1u, d.size()); + ASSERT_EQ("test3", d[0].asString()); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/world")); + ASSERT_EQ(2u, d.size()); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/a/test3")); + ASSERT_EQ(1u, d.size()); + ASSERT_EQ("test4", d[0].asString()); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test")); + ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test2")); + ASSERT_FALSE(GetDirectory(d, root, "/hello2")); + + testValue = 0; + ASSERT_TRUE(HandleGet(root, "/hello/world/test")); + ASSERT_EQ(testValue, 1); + ASSERT_TRUE(HandleGet(root, "/hello/world/test2")); + ASSERT_EQ(testValue, 2); + ASSERT_TRUE(HandleGet(root, "/hello/b/test3/test4")); + ASSERT_EQ(testValue, 3); + ASSERT_FALSE(HandleGet(root, "/hello/b/test3/test")); + ASSERT_EQ(testValue, 3); + ASSERT_TRUE(HandleGet(root, "/hello2/a/b")); + ASSERT_EQ(testValue, 4); +} + + + + + +namespace +{ + class AcceptHandler : public HttpContentNegociation::IHandler + { + private: + std::string type_; + std::string subtype_; + + public: + AcceptHandler() + { + Reset(); + } + + void Reset() + { + Handle("nope", "nope"); + } + + const std::string& GetType() const + { + return type_; + } + + const std::string& GetSubType() const + { + return subtype_; + } + + virtual void Handle(const std::string& type, + const std::string& subtype) ORTHANC_OVERRIDE + { + type_ = type; + subtype_ = subtype; + } + }; +} + + +TEST(RestApi, HttpContentNegociation) +{ + // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + + AcceptHandler h; + + { + HttpContentNegociation d; + d.Register("audio/mp3", h); + d.Register("audio/basic", h); + + ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/basic")); + ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ("basic", h.GetSubType()); + + ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/nope")); + ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ("mp3", h.GetSubType()); + + ASSERT_FALSE(d.Apply("application/*; q=0.2, application/pdf")); + + ASSERT_TRUE(d.Apply("*/*; application/*; q=0.2, application/pdf")); + ASSERT_EQ("audio", h.GetType()); + } + + // "This would be interpreted as "text/html and text/x-c are the + // preferred media types, but if they do not exist, then send the + // text/x-dvi entity, and if that does not exist, send the + // text/plain entity."" + const std::string T1 = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"; + + { + HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/html", h); + d.Register("text/x-dvi", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("html", h.GetSubType()); + } + + { + HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + d.Register("text/x-c", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("x-c", h.GetSubType()); + } + + { + HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + d.Register("text/x-c", h); + d.Register("text/html", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_TRUE(h.GetSubType() == "x-c" || h.GetSubType() == "html"); + } + + { + HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("x-dvi", h.GetSubType()); + } + + { + HttpContentNegociation d; + d.Register("text/plain", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("plain", h.GetSubType()); + } +} + + +TEST(WebServiceParameters, Serialization) +{ + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + + Json::Value v2; + p.Serialize(v2, false, true); + ASSERT_EQ(v, v2); + + WebServiceParameters p2(v2); + ASSERT_EQ("http://localhost:8042/", p2.GetUrl()); + ASSERT_TRUE(p2.GetUsername().empty()); + ASSERT_TRUE(p2.GetPassword().empty()); + ASSERT_TRUE(p2.GetCertificateFile().empty()); + ASSERT_TRUE(p2.GetCertificateKeyFile().empty()); + ASSERT_TRUE(p2.GetCertificateKeyPassword().empty()); + ASSERT_FALSE(p2.IsPkcs11Enabled()); + } + + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + v.append("user"); + v.append("pass"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + ASSERT_EQ("http://localhost:8042/", p.GetUrl()); + ASSERT_EQ("user", p.GetUsername()); + ASSERT_EQ("pass", p.GetPassword()); + ASSERT_TRUE(p.GetCertificateFile().empty()); + ASSERT_TRUE(p.GetCertificateKeyFile().empty()); + ASSERT_TRUE(p.GetCertificateKeyPassword().empty()); + ASSERT_FALSE(p.IsPkcs11Enabled()); + + Json::Value v2; + p.Serialize(v2, false, true); + ASSERT_EQ(v, v2); + + p.Serialize(v2, false, false /* no password */); + WebServiceParameters p2(v2); + ASSERT_EQ(Json::arrayValue, v2.type()); + ASSERT_EQ(3u, v2.size()); + ASSERT_EQ("http://localhost:8042/", v2[0u].asString()); + ASSERT_EQ("user", v2[1u].asString()); + ASSERT_TRUE(v2[2u].asString().empty()); + } + + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + p.SetPkcs11Enabled(true); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + + Json::Value v2; + p.Serialize(v2, false, true); + WebServiceParameters p2(v2); + + ASSERT_EQ(Json::objectValue, v2.type()); + ASSERT_EQ(3u, v2.size()); + ASSERT_EQ("http://localhost:8042/", v2["Url"].asString()); + ASSERT_TRUE(v2["Pkcs11"].asBool()); + ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type()); + ASSERT_EQ(0u, v2["HttpHeaders"].size()); + } + + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + p.SetClientCertificate("a", "b", "c"); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + + Json::Value v2; + p.Serialize(v2, false, true); + WebServiceParameters p2(v2); + + ASSERT_EQ(Json::objectValue, v2.type()); + ASSERT_EQ(6u, v2.size()); + ASSERT_EQ("http://localhost:8042/", v2["Url"].asString()); + ASSERT_EQ("a", v2["CertificateFile"].asString()); + ASSERT_EQ("b", v2["CertificateKeyFile"].asString()); + ASSERT_EQ("c", v2["CertificateKeyPassword"].asString()); + ASSERT_FALSE(v2["Pkcs11"].asBool()); + ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type()); + ASSERT_EQ(0u, v2["HttpHeaders"].size()); + } + + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + p.AddHttpHeader("a", "b"); + p.AddHttpHeader("c", "d"); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + + Json::Value v2; + p.Serialize(v2, false, true); + WebServiceParameters p2(v2); + + ASSERT_EQ(Json::objectValue, v2.type()); + ASSERT_EQ(3u, v2.size()); + ASSERT_EQ("http://localhost:8042/", v2["Url"].asString()); + ASSERT_FALSE(v2["Pkcs11"].asBool()); + ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type()); + ASSERT_EQ(2u, v2["HttpHeaders"].size()); + ASSERT_EQ("b", v2["HttpHeaders"]["a"].asString()); + ASSERT_EQ("d", v2["HttpHeaders"]["c"].asString()); + + std::set<std::string> a; + p2.ListHttpHeaders(a); + ASSERT_EQ(2u, a.size()); + ASSERT_TRUE(a.find("a") != a.end()); + ASSERT_TRUE(a.find("c") != a.end()); + + std::string s; + ASSERT_TRUE(p2.LookupHttpHeader(s, "a")); ASSERT_EQ("b", s); + ASSERT_TRUE(p2.LookupHttpHeader(s, "c")); ASSERT_EQ("d", s); + ASSERT_FALSE(p2.LookupHttpHeader(s, "nope")); + } +} + + +TEST(WebServiceParameters, UserProperties) +{ + Json::Value v = Json::nullValue; + + { + WebServiceParameters p; + p.SetUrl("http://localhost:8042/"); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + + ASSERT_THROW(p.AddUserProperty("Url", "nope"), OrthancException); + p.AddUserProperty("Hello", "world"); + p.AddUserProperty("a", "b"); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + + p.Serialize(v, false, true); + + p.ClearUserProperties(); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + } + + { + WebServiceParameters p(v); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + ASSERT_TRUE(p.GetHttpHeaders().empty()); + + std::set<std::string> tmp; + p.ListUserProperties(tmp); + ASSERT_EQ(2u, tmp.size()); + ASSERT_TRUE(tmp.find("a") != tmp.end()); + ASSERT_TRUE(tmp.find("Hello") != tmp.end()); + ASSERT_TRUE(tmp.find("hello") == tmp.end()); + + std::string s; + ASSERT_TRUE(p.LookupUserProperty(s, "a")); ASSERT_TRUE(s == "b"); + ASSERT_TRUE(p.LookupUserProperty(s, "Hello")); ASSERT_TRUE(s == "world"); + ASSERT_FALSE(p.LookupUserProperty(s, "hello")); + } +} + + +TEST(StringMatcher, Basic) +{ + StringMatcher matcher("---"); + + ASSERT_THROW(matcher.GetMatchBegin(), OrthancException); + + { + const std::string s = ""; + ASSERT_FALSE(matcher.Apply(s)); + } + + { + const std::string s = "abc----def"; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin())); + ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd())); + } + + { + const std::string s = "abc---"; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin())); + ASSERT_EQ(s.end(), matcher.GetMatchEnd()); + ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd())); + ASSERT_EQ("", std::string(matcher.GetMatchEnd(), s.end())); + } + + { + const std::string s = "abc--def"; + ASSERT_FALSE(matcher.Apply(s)); + ASSERT_THROW(matcher.GetMatchBegin(), OrthancException); + ASSERT_THROW(matcher.GetMatchEnd(), OrthancException); + } + + { + std::string s(10u, '\0'); // String with null values + ASSERT_EQ(10u, s.size()); + ASSERT_EQ(10u, s.size()); + ASSERT_FALSE(matcher.Apply(s)); + + s[9] = '-'; + ASSERT_FALSE(matcher.Apply(s)); + + s[8] = '-'; + ASSERT_FALSE(matcher.Apply(s)); + + s[7] = '-'; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(s.c_str() + 7, matcher.GetPointerBegin()); + ASSERT_EQ(s.c_str() + 10, matcher.GetPointerEnd()); + ASSERT_EQ(s.end() - 3, matcher.GetMatchBegin()); + ASSERT_EQ(s.end(), matcher.GetMatchEnd()); + } +} + + + +class MultipartTester : public MultipartStreamReader::IHandler +{ +private: + struct Part + { + MultipartStreamReader::HttpHeaders headers_; + std::string data_; + + Part(const MultipartStreamReader::HttpHeaders& headers, + const void* part, + size_t size) : + headers_(headers), + data_(reinterpret_cast<const char*>(part), size) + { + } + }; + + std::vector<Part> parts_; + +public: + virtual void HandlePart(const MultipartStreamReader::HttpHeaders& headers, + const void* part, + size_t size) + { + parts_.push_back(Part(headers, part, size)); + } + + unsigned int GetCount() const + { + return parts_.size(); + } + + MultipartStreamReader::HttpHeaders& GetHeaders(size_t i) + { + return parts_[i].headers_; + } + + const std::string& GetData(size_t i) const + { + return parts_[i].data_; + } +}; + + +TEST(MultipartStreamReader, ParseHeaders) +{ + std::string ct, b, st, header; + + { + MultipartStreamReader::HttpHeaders h; + h["hello"] = "world"; + h["Content-Type"] = "world"; // Should be in lower-case + h["CONTENT-type"] = "world"; // Should be in lower-case + ASSERT_FALSE(MultipartStreamReader::GetMainContentType(header, h)); + } + + { + MultipartStreamReader::HttpHeaders h; + h["content-type"] = "world"; + ASSERT_TRUE(MultipartStreamReader::GetMainContentType(header, h)); + ASSERT_EQ(header, "world"); + ASSERT_FALSE(MultipartStreamReader::ParseMultipartContentType(ct, st, b, header)); + } + + { + MultipartStreamReader::HttpHeaders h; + h["content-type"] = "multipart/related; dummy=value; boundary=1234; hello=world"; + ASSERT_TRUE(MultipartStreamReader::GetMainContentType(header, h)); + ASSERT_EQ(header, h["content-type"]); + ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType(ct, st, b, header)); + ASSERT_EQ(ct, "multipart/related"); + ASSERT_EQ(b, "1234"); + ASSERT_TRUE(st.empty()); + } + + { + ASSERT_FALSE(MultipartStreamReader::ParseMultipartContentType + (ct, st, b, "multipart/related; boundary=")); // Empty boundary + } + + { + ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType + (ct, st, b, "Multipart/Related; TYPE=Application/Dicom; Boundary=heLLO")); + ASSERT_EQ(ct, "multipart/related"); + ASSERT_EQ(b, "heLLO"); + ASSERT_EQ(st, "application/dicom"); + } + + { + ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType + (ct, st, b, "Multipart/Related; type=\"application/DICOM\"; Boundary=a")); + ASSERT_EQ(ct, "multipart/related"); + ASSERT_EQ(b, "a"); + ASSERT_EQ(st, "application/dicom"); + } +} + + +TEST(MultipartStreamReader, BytePerByte) +{ + std::string stream = "GARBAGE"; + + std::string boundary = "123456789123456789"; + + { + for (size_t i = 0; i < 10; i++) + { + std::string f = "hello " + boost::lexical_cast<std::string>(i); + + stream += "\r\n--" + boundary + "\r\n"; + if (i % 2 == 0) + stream += "Content-Length: " + boost::lexical_cast<std::string>(f.size()) + "\r\n"; + stream += "Content-Type: toto " + boost::lexical_cast<std::string>(i) + "\r\n\r\n"; + stream += f; + } + + stream += "\r\n--" + boundary + "--"; + stream += "GARBAGE"; + } + + for (unsigned int k = 0; k < 2; k++) + { + MultipartTester decoded; + + MultipartStreamReader reader(boundary); + reader.SetBlockSize(1); + reader.SetHandler(decoded); + + if (k == 0) + { + for (size_t i = 0; i < stream.size(); i++) + { + reader.AddChunk(&stream[i], 1); + } + } + else + { + reader.AddChunk(stream); + } + + reader.CloseStream(); + + ASSERT_EQ(10u, decoded.GetCount()); + + for (size_t i = 0; i < 10; i++) + { + ASSERT_EQ("hello " + boost::lexical_cast<std::string>(i), decoded.GetData(i)); + ASSERT_EQ("toto " + boost::lexical_cast<std::string>(i), decoded.GetHeaders(i)["content-type"]); + + if (i % 2 == 0) + { + ASSERT_EQ(2u, decoded.GetHeaders(i).size()); + ASSERT_TRUE(decoded.GetHeaders(i).find("content-length") != decoded.GetHeaders(i).end()); + } + } + } +}