chromium/chromeos/printing/ppd_metadata_parser_unittest.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/printing/ppd_metadata_parser.h"

#include <string_view>

#include "base/json/json_reader.h"
#include "chromeos/printing/ppd_metadata_matchers.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace chromeos {
namespace {

using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::ExplainMatchResult;
using ::testing::Field;
using ::testing::Ne;
using ::testing::Optional;
using ::testing::Pair;
using ::testing::StrEq;
using ::testing::UnorderedElementsAre;

constexpr std::string_view kInvalidJson = "blah blah invalid JSON";

// Verifies that ParseLocales() can parse locales metadata.
TEST(PpdMetadataParserTest, CanParseLocales) {
  constexpr std::string_view kLocalesJson = R"(
  {
    "locales": [ "de", "en", "es", "jp" ]
  }
  )";

  const auto parsed = ParseLocales(kLocalesJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(*parsed,
              ElementsAre(StrEq("de"), StrEq("en"), StrEq("es"), StrEq("jp")));
}

// Verifies that ParseLocales() can parse locales and return a partial
// list even when it encounters unexpected values.
TEST(PpdMetadataParserTest, CanPartiallyParseLocales) {
  // The values "0.0" and "78" are gibberish that ParseLocales() shall
  // ignore; however, these don't structurally foul the JSON, so it can
  // still return the other locales.
  constexpr std::string_view kLocalesJson = R"(
  {
    "locales": [ 0.0, "de", 78, "en", "es", "jp" ]
  }
  )";

  const auto parsed = ParseLocales(kLocalesJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(*parsed,
              ElementsAre(StrEq("de"), StrEq("en"), StrEq("es"), StrEq("jp")));
}

// Verifies that ParseLocales() returns std::nullopt rather than an
// empty container.
TEST(PpdMetadataParserTest, ParseLocalesDoesNotReturnEmptyContainer) {
  // The values "0.0" and "78" are gibberish that ParseLocales() shall
  // ignore; while the JSON is still well-formed, the parsed list of
  // locales contains no values.
  constexpr std::string_view kLocalesJson = R"(
  {
    "locales": [ 0.0, 78 ]
  }
  )";

  EXPECT_FALSE(ParseLocales(kLocalesJson).has_value());
}

// Verifies that ParseLocales() returns std::nullopt on irrecoverable
// parse error.
TEST(PpdMetadataParserTest, ParseLocalesFailsGracefully) {
  EXPECT_FALSE(ParseLocales(kInvalidJson).has_value());
}

// Verifies that ParseManufacturers() can parse manufacturers metadata.
TEST(PpdMetadataParserTest, CanParseManufacturers) {
  constexpr std::string_view kManufacturersJson = R"(
  {
    "filesMap": {
      "Andante": "andante-en.json",
      "Sostenuto": "sostenuto-en.json"
    }
  }
  )";

  const auto parsed = ParseManufacturers(kManufacturersJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(*parsed,
              UnorderedElementsAre(
                  Pair(StrEq("Andante"), StrEq("andante-en.json")),
                  Pair(StrEq("Sostenuto"), StrEq("sostenuto-en.json"))));
}

// Verifies that ParseManufacturers() can parse manufacturers and return
// a partial list even when it encounters unexpected values.
TEST(PpdMetadataParserTest, CanPartiallyParseManufacturers) {
  // Contains an embedded dictionary keyed on "Dearie me."
  // ParseManufacturers() shall ignore this.
  constexpr std::string_view kManufacturersJson = R"(
  {
    "filesMap": {
      "Dearie me": {
        "I didn't": "expect",
        "to go": "deeper"
      },
      "Andante": "andante-en.json",
      "Sostenuto": "sostenuto-en.json"
    }
  }
  )";

  const auto parsed = ParseManufacturers(kManufacturersJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(*parsed,
              UnorderedElementsAre(
                  Pair(StrEq("Andante"), StrEq("andante-en.json")),
                  Pair(StrEq("Sostenuto"), StrEq("sostenuto-en.json"))));
}

// Verifies that ParseManufacturers() returns std::nullopt rather than
// an empty container.
TEST(PpdMetadataParserTest, ParseManufacturersDoesNotReturnEmptyContainer) {
  // Contains an embedded dictionary keyed on "Dearie me."
  // ParseManufacturers() shall ignore this, but in doing so shall leave
  // its ParsedManufacturers return value empty.
  constexpr std::string_view kManufacturersJson = R"(
  {
    "filesMap": {
      "Dearie me": {
        "I didn't": "expect",
        "to go": "deeper"
      }
    }
  }
  )";

  EXPECT_FALSE(ParseManufacturers(kManufacturersJson).has_value());
}

// Verifies that ParseManufacturers() returns std::nullopt on
// irrecoverable parse error.
TEST(PpdMetadataParserTest, ParseManufacturersFailsGracefully) {
  EXPECT_FALSE(ParseManufacturers(kInvalidJson).has_value());
}

// Verifies that ParsePrinters() can parse printers metadata.
TEST(PpdMetadataParserTest, CanParsePrinters) {
  constexpr std::string_view kPrintersJson = R"(
  {
    "printers": [ {
      "emm": "d 547b",
      "name": "An die Musik"
    }, {
      "emm": "d 553",
      "name": "Auf der Donau"
    } ]
  }
  )";

  const auto parsed = ParsePrinters(kPrintersJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(*parsed, UnorderedElementsAre(
                           ParsedPrinterLike("An die Musik", "d 547b"),
                           ParsedPrinterLike("Auf der Donau", "d 553")));
}

// Verifies that ParsePrinters() can parse printers and return a partial
// list even when it encounters unexpected values.
TEST(PpdMetadataParserTest, CanPartiallyParsePrinters) {
  // Contains an extra value keyed on "hello" in an otherwise valid leaf
  // value in Printers metadata. ParsePrinters() shall ignore this.
  constexpr std::string_view kPrintersJson = R"(
  {
    "printers": [ {
      "emm": "d 552",
      "name": "Hänflings Liebeswerbung",
      "hello": "there!"
    }, {
      "emm": "d 553",
      "name": "Auf der Donau"
    } ]
  }
  )";

  const auto parsed = ParsePrinters(kPrintersJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(*parsed,
              UnorderedElementsAre(
                  ParsedPrinterLike("Hänflings Liebeswerbung", "d 552"),
                  ParsedPrinterLike("Auf der Donau", "d 553")));
}

// Verifies that ParsePrinters() can parse printers and their
// well-formed restrictions (if any are specified).
TEST(PpdMetadataParserTest, CanParsePrintersWithRestrictions) {
  // Specifies
  // *  a printer with a minimum milestone,
  // *  a printer with a maximum milestone, and
  // *  a printer with both minimum and maximum milestones.
  constexpr std::string_view kPrintersJson = R"(
  {
    "printers": [ {
      "emm": "d 121",
      "name": "Schäfers Klagelied",
      "restriction": {
        "minMilestone": 121
      }
    }, {
      "emm": "d 216",
      "name": "Meeres Stille",
      "restriction": {
        "maxMilestone": 216
      }
    }, {
      "emm": "d 257",
      "name": "Heidenröslein",
      "restriction": {
        "minMilestone": 216,
        "maxMilestone": 257
      }
    } ]
  }
  )";

  const auto parsed = ParsePrinters(kPrintersJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(*parsed,
              UnorderedElementsAre(
                  AllOf(ParsedPrinterLike("Schäfers Klagelied", "d 121"),
                        Field(&ParsedPrinter::restrictions,
                              RestrictionsWithMinMilestone(121))),
                  AllOf(ParsedPrinterLike("Meeres Stille", "d 216"),
                        Field(&ParsedPrinter::restrictions,
                              RestrictionsWithMaxMilestone(216))),
                  AllOf(ParsedPrinterLike("Heidenröslein", "d 257"),
                        Field(&ParsedPrinter::restrictions,
                              RestrictionsWithMinAndMaxMilestones(216, 257)))));
}

// Verifies that ParsePrinters() can parse printers and ignore
// malformed restrictions.
TEST(PpdMetadataParserTest, CanParsePrintersWithMalformedRestrictions) {
  // Specifies a printer with invalid restrictions.
  constexpr std::string_view kPrintersJson = R"(
  {
    "printers": [ {
      "emm": "d 368",
      "name": "Jägers Abendlied",
      "restriction": {
        "hello": "there!"
      }
    } ]
  }
  )";

  const auto parsed = ParsePrinters(kPrintersJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(
      *parsed,
      UnorderedElementsAre(
          AllOf(ParsedPrinterLike("Jägers Abendlied", "d 368"),
                Field(&ParsedPrinter::restrictions, UnboundedRestrictions()))));
}

// Verifies that ParsePrinters() returns std::nullopt rather than an
// empty container.
TEST(PpdMetadataParserTest, ParsePrintersDoesNotReturnEmptyContainer) {
  // No printers are specified in this otherwise valid JSON dictionary.
  EXPECT_FALSE(ParsePrinters("{}").has_value());
}

// Verifies that ParsePrinters() returns std::nullopt on irrecoverable
// parse error.
TEST(PpdMetadataParserTest, ParsePrintersFailsGracefully) {
  EXPECT_FALSE(ParsePrinters(kInvalidJson).has_value());
}

// Verifies that ParseForwardIndex() can parse forward index metadata.
TEST(PpdMetadataParserTest, CanParseForwardIndex) {
  constexpr std::string_view kJsonForwardIndex = R"({
  "ppdIndex": {
    "der wanderer": {
      "ppdMetadata": [ {
        "name": "d-489.ppd.gz"
      } ]
    },
    "morgenlied": {
      "ppdMetadata": [ {
        "name": "d-685.ppd.gz"
      } ]
    },
    "wandrers nachtlied": {
      "ppdMetadata": [ {
        "name": "d-224.ppd.gz"
      } ]
    }
  }
})";

  const auto parsed = ParseForwardIndex(kJsonForwardIndex);
  ASSERT_TRUE(parsed.has_value());
  EXPECT_THAT(
      parsed.value(),
      UnorderedElementsAre(
          ParsedIndexEntryLike(
              "der wanderer",
              UnorderedElementsAre(
                  ParsedIndexLeafWithPpdBasename("d-489.ppd.gz"))),
          ParsedIndexEntryLike(
              "morgenlied", UnorderedElementsAre(ParsedIndexLeafWithPpdBasename(
                                "d-685.ppd.gz"))),
          ParsedIndexEntryLike(
              "wandrers nachtlied",
              UnorderedElementsAre(
                  ParsedIndexLeafWithPpdBasename("d-224.ppd.gz")))));
}

// Verifies that ParseForwardIndex() can parse forward index metadata
// and return a partial list even when it encounters unexpected values.
TEST(PpdMetadataParserTest, CanPartiallyParseForwardIndex) {
  // Uses the same value as the CanParseForwardIndex test, but
  // with garbage values mixed in.
  constexpr std::string_view kJsonForwardIndex = R"({
  "ppdIndex": {
    "garbage": "unused value",
    "more garbage": [ "more", "unused", "values" ],
    "der wanderer": {
      "ppdMetadata": [ {
        "name": "d-489.ppd.gz",
        "more garbage still": "unused value"
      }, {
        "also garbage": "unused value"
      } ],
      "unending garbage": "unused value"
    },
    "morgenlied": {
      "ppdMetadata": [ {
        "name": "d-685.ppd.gz"
      } ]
    },
    "wandrers nachtlied": {
      "ppdMetadata": [ {
        "name": "d-224.ppd.gz"
      } ]
    }
  }
})";

  const auto parsed = ParseForwardIndex(kJsonForwardIndex);
  ASSERT_TRUE(parsed.has_value());
  EXPECT_THAT(
      parsed.value(),
      UnorderedElementsAre(
          ParsedIndexEntryLike(
              "der wanderer",
              UnorderedElementsAre(
                  ParsedIndexLeafWithPpdBasename("d-489.ppd.gz"))),
          ParsedIndexEntryLike(
              "morgenlied", UnorderedElementsAre(ParsedIndexLeafWithPpdBasename(
                                "d-685.ppd.gz"))),
          ParsedIndexEntryLike(
              "wandrers nachtlied",
              UnorderedElementsAre(
                  ParsedIndexLeafWithPpdBasename("d-224.ppd.gz")))));
}

// Verifies that ParseForwardIndex() can parse forward index metadata
// in which leaf values have multiple ppdMetadata key-value pairs.
TEST(PpdMetadataParserTest, CanParseForwardIndexWithMultiplePpdMetadataLeafs) {
  constexpr std::string_view kJsonForwardIndex = R"({
  "ppdIndex": {
    "rastlose liebe": {
      "ppdMetadata": [ {
        "name": "d-138.ppd.gz"
      }, {
        "name": "1815.ppd.gz"
      } ]
    }
  }
})";

  const auto parsed = ParseForwardIndex(kJsonForwardIndex);
  ASSERT_TRUE(parsed.has_value());
  EXPECT_THAT(parsed.value(),
              UnorderedElementsAre(ParsedIndexEntryLike(
                  "rastlose liebe",
                  UnorderedElementsAre(
                      ParsedIndexLeafWithPpdBasename("d-138.ppd.gz"),
                      ParsedIndexLeafWithPpdBasename("1815.ppd.gz")))));
}

// Verifies that ParseForwardIndex() can parse forward index metadata
// and its well-formed restrictions (if any are specified).
TEST(PpdMetadataParserTest, CanParseForwardIndexWithRestrictions) {
  // Specifies
  // *  a PPD metadata leaf with a minimum milestone,
  // *  a PPD metadata leaf with a maximum milestone, and
  // *  a PPD metadata leaf with both minimum and maximum milestones.
  constexpr std::string_view kJsonForwardIndex = R"({
  "ppdIndex": {
    "nähe des geliebten": {
      "ppdMetadata": [ {
        "name": "d-162.ppd.gz",
        "restriction": {
          "minMilestone": 25
        }
      } ]
    },
    "der fischer": {
      "ppdMetadata": [ {
        "name": "d-225.ppd.gz",
        "restriction": {
          "maxMilestone": 35
        }
      } ]
    },
    "erster verlust": {
      "ppdMetadata": [ {
        "name": "d-226.ppd.gz",
        "restriction": {
          "minMilestone": 45,
          "maxMilestone": 46
        }
      } ]
    }
  }
})";

  const auto parsed = ParseForwardIndex(kJsonForwardIndex);
  ASSERT_TRUE(parsed.has_value());
  EXPECT_THAT(
      parsed.value(),
      UnorderedElementsAre(
          ParsedIndexEntryLike(
              "nähe des geliebten",
              UnorderedElementsAre(
                  AllOf(ParsedIndexLeafWithPpdBasename("d-162.ppd.gz"),
                        Field(&ParsedIndexLeaf::restrictions,
                              RestrictionsWithMinMilestone(25))))),
          ParsedIndexEntryLike(
              "der fischer", UnorderedElementsAre(AllOf(
                                 ParsedIndexLeafWithPpdBasename("d-225.ppd.gz"),
                                 Field(&ParsedIndexLeaf::restrictions,
                                       RestrictionsWithMaxMilestone(35))))),
          ParsedIndexEntryLike(
              "erster verlust",
              UnorderedElementsAre(
                  AllOf(ParsedIndexLeafWithPpdBasename("d-226.ppd.gz"),
                        Field(&ParsedIndexLeaf::restrictions,
                              RestrictionsWithMinAndMaxMilestones(45, 46)))))));
}

// Verifies that ParseForwardIndex() can parse forward index metadata
// and ignore malformed restrictions.
TEST(PpdMetadataParserTest, CanParseForwardIndexWithMalformedRestrictions) {
  // Same test data as the CanParseForwardIndexWithRestrictions test
  // above, but defines
  // *  a PPD metadata leaf with a malformed minimum milestone,
  // *  a PPD metadata leaf with a malformed maximum milestone, and
  // *  a PPD metadata leaf with malformed minimum and maximum
  //    milestones.
  constexpr std::string_view kJsonForwardIndex = R"({
  "ppdIndex": {
    "nähe des geliebten": {
      "ppdMetadata": [ {
        "name": "d-162.ppd.gz",
        "restriction": {
          "minMilestone": "garbage value",
          "maxMilestone": 25
        }
      } ]
    },
    "der fischer": {
      "ppdMetadata": [ {
        "name": "d-225.ppd.gz",
        "restriction": {
          "minMilestone": 35,
          "maxMilestone": "garbage value"
        }
      } ]
    },
    "erster verlust": {
      "ppdMetadata": [ {
        "name": "d-226.ppd.gz",
        "restriction": {
          "minMilestone": "garbage value",
          "maxMilestone": "garbage value"
        }
      } ]
    }
  }
})";

  const auto parsed = ParseForwardIndex(kJsonForwardIndex);
  ASSERT_TRUE(parsed.has_value());
  EXPECT_THAT(
      parsed.value(),
      UnorderedElementsAre(
          ParsedIndexEntryLike(
              "nähe des geliebten",
              UnorderedElementsAre(
                  AllOf(ParsedIndexLeafWithPpdBasename("d-162.ppd.gz"),
                        Field(&ParsedIndexLeaf::restrictions,
                              RestrictionsWithMaxMilestone(25))))),
          ParsedIndexEntryLike(
              "der fischer", UnorderedElementsAre(AllOf(
                                 ParsedIndexLeafWithPpdBasename("d-225.ppd.gz"),
                                 Field(&ParsedIndexLeaf::restrictions,
                                       RestrictionsWithMinMilestone(35))))),
          ParsedIndexEntryLike(
              "erster verlust",
              UnorderedElementsAre(
                  AllOf(ParsedIndexLeafWithPpdBasename("d-226.ppd.gz"),
                        Field(&ParsedIndexLeaf::restrictions,
                              UnboundedRestrictions()))))));
}

// Verifies that ParseForwardIndex() can parse forward index metadata
// specifying PPD licenses.
TEST(PpdMetadataParserTest, CanParseForwardIndexWithLicenses) {
  // Specifies two PPDs, each with a license associated.
  constexpr std::string_view kJsonForwardIndex = R"({
  "ppdIndex": {
    "der fischer": {
      "ppdMetadata": [ {
        "name": "d-225.ppd.gz",
        "license": "two two five license"
      } ]
    },
    "erster verlust": {
      "ppdMetadata": [ {
        "name": "d-226.ppd.gz",
        "license": "two two six license"
      } ]
    }
  }
})";
  const auto parsed = ParseForwardIndex(kJsonForwardIndex);
  ASSERT_TRUE(parsed.has_value());
  EXPECT_THAT(
      parsed.value(),
      UnorderedElementsAre(
          ParsedIndexEntryLike(
              "der fischer", UnorderedElementsAre(AllOf(
                                 ParsedIndexLeafWithPpdBasename("d-225.ppd.gz"),
                                 Field(&ParsedIndexLeaf::license,
                                       StrEq("two two five license"))))),
          ParsedIndexEntryLike(
              "erster verlust",
              UnorderedElementsAre(
                  AllOf(ParsedIndexLeafWithPpdBasename("d-226.ppd.gz"),
                        Field(&ParsedIndexLeaf::license,
                              StrEq("two two six license")))))));
}

// Verifies that ParseForwardIndex() returns std::nullopt rather than
// an empty container.
TEST(PpdMetadataParserTest, ParseForwardIndexDoesNotReturnEmptyContainer) {
  // Specifies a forward index that is valid JSON but which has
  // no PPDs whose leaf values are non-empty.
  constexpr std::string_view kJsonForwardIndex = R"({
  "ppdIndex": {
    "der könig in thule": {
      "ppdMetadata": [ {
        "garbage": "unused value"
      } ]
    }
  }
})";

  EXPECT_FALSE(ParseForwardIndex(kJsonForwardIndex).has_value());
}

// Verifies that ParseForwardIndex() returns std::nullopt on
// irrecoverable parse error.
TEST(PpdMetadataParserTest, ParseForwardIndexFailsGracefully) {
  EXPECT_FALSE(ParseForwardIndex(kInvalidJson).has_value());
}

// Verifies that ParseUsbIndex() can parse USB index metadata.
TEST(PpdMetadataParserTest, CanParseUsbIndex) {
  constexpr std::string_view kJsonUsbIndex = R"({
  "usbIndex": {
    "1": {
      "effectiveMakeAndModel": "d 541"
    },
    "10": {
      "effectiveMakeAndModel": "d 504"
    }
  }
})";

  EXPECT_THAT(ParseUsbIndex(kJsonUsbIndex),
              Optional(UnorderedElementsAre(Pair(1, StrEq("d 541")),
                                            Pair(10, StrEq("d 504")))));
}

// Verifies that ParseUsbIndex() can parse USB index metadata and return
// a partial ParsedUsbIndex even when it encounters garbage values.
TEST(PpdMetadataParserTest, CanPartiallyParseUsbIndex) {
  constexpr std::string_view kJsonUsbIndex = R"({
  "usbIndex": {
    "garbage key": {
      "effectiveMakeAndModel": "garbage value"
    },
    "1": {
      "effectiveMakeAndModel": "d 541"
    },
    "10": {
      "effectiveMakeAndModel": "d 504"
    }
  }
})";

  EXPECT_THAT(ParseUsbIndex(kJsonUsbIndex),
              Optional(UnorderedElementsAre(Pair(1, StrEq("d 541")),
                                            Pair(10, StrEq("d 504")))));
}

// Verifies that ParseUsbIndex() returns std::nullopt rather than an
// empty container.
TEST(PpdMetadataParserTest, ParseUsbIndexDoesNotReturnEmptyContainer) {
  constexpr std::string_view kEmptyJsonUsbIndex = R"({
  "usbIndex": { }
})";
  ASSERT_THAT(base::JSONReader::Read(kEmptyJsonUsbIndex), Ne(std::nullopt));
  EXPECT_THAT(ParseUsbIndex(kEmptyJsonUsbIndex), Eq(std::nullopt));

  constexpr std::string_view kJsonUsbIndexWithBadStringKeys = R"({
  "usbIndex": {
    "non-integral key": { }
  }
})";
  ASSERT_THAT(base::JSONReader::Read(kJsonUsbIndexWithBadStringKeys),
              Ne(std::nullopt));
  EXPECT_THAT(ParseUsbIndex(kJsonUsbIndexWithBadStringKeys), Eq(std::nullopt));

  constexpr std::string_view kJsonUsbIndexWithoutEmmAtLeaf = R"({
  "usbIndex": {
    "1": {
      "some key that is not ``effectiveMakeAndModel''": "d 504"
    }
  }
})";
  ASSERT_THAT(base::JSONReader::Read(kJsonUsbIndexWithoutEmmAtLeaf),
              Ne(std::nullopt));
  EXPECT_THAT(ParseUsbIndex(kJsonUsbIndexWithoutEmmAtLeaf), Eq(std::nullopt));

  constexpr std::string_view kJsonUsbIndexWithEmptyEmmAtLeaf = R"({
  "usbIndex": {
    "1": {
      "effectiveMakeAndModel": ""
    }
  }
})";
  ASSERT_THAT(base::JSONReader::Read(kJsonUsbIndexWithEmptyEmmAtLeaf),
              Ne(std::nullopt));
  EXPECT_THAT(ParseUsbIndex(kJsonUsbIndexWithEmptyEmmAtLeaf), Eq(std::nullopt));
}

// Verifies that ParseUsbIndex() returns std::nullopt on irrecoverable
// parse error.
TEST(PpdMetadataParserTest, ParseUsbIndexFailsGracefully) {
  EXPECT_THAT(ParseUsbIndex(kInvalidJson), Eq(std::nullopt));
}

// Verifies that ParseUsbvendorIdMap() can parse USB vendor ID maps.
TEST(PpdMetadataParserTest, CanParseUsbVendorIdMap) {
  constexpr std::string_view kJsonUsbVendorIdMap = R"({
  "entries": [ {
    "vendorId": 1111,
    "vendorName": "One One One One"
  }, {
    "vendorId": 2222,
    "vendorName": "Two Two Two Two"
  }, {
    "vendorId": 3333,
    "vendorName": "Three Three Three Three"
  } ]
})";

  EXPECT_THAT(ParseUsbVendorIdMap(kJsonUsbVendorIdMap),
              Optional(UnorderedElementsAre(
                  Pair(1111, StrEq("One One One One")),
                  Pair(2222, StrEq("Two Two Two Two")),
                  Pair(3333, StrEq("Three Three Three Three")))));
}

// Verifies that ParseUsbvendorIdMap() can parse USB vendor ID maps and
// return partial results even when it encounters garbage values.
TEST(PpdMetadataParserTest, CanPartiallyParseUsbVendorIdMap) {
  // This USB vendor ID map has garbage values in it.
  // ParseUsbVendorIdMap() shall ignore these.
  constexpr std::string_view kJsonUsbVendorIdMap = R"({
  "garbage key": "garbage value",
  "entries": [
    "garbage value",
  {
    "vendorId": 1111,
    "garbage key": "garbage value",
    "vendorName": "One One One One"
  }, {
    "vendorId": 2222,
    "vendorName": "Two Two Two Two"
  }, {
    "vendorId": 3333,
    "vendorName": "Three Three Three Three"
  } ]
})";

  EXPECT_THAT(ParseUsbVendorIdMap(kJsonUsbVendorIdMap),
              Optional(UnorderedElementsAre(
                  Pair(1111, StrEq("One One One One")),
                  Pair(2222, StrEq("Two Two Two Two")),
                  Pair(3333, StrEq("Three Three Three Three")))));
}

// Verifies that ParseUsbvendorIdMap() returns std::nullopt rather
// than an empty container.
TEST(PpdMetadataParserTest, ParseUsbVendorIdMapDoesNotReturnEmptyContainer) {
  // Defines a USB vendor ID map that is empty; it's valid JSON, but
  // has no values worth returning.
  constexpr std::string_view kJsonUsbVendorIdMap = "{}";

  EXPECT_THAT(ParseUsbVendorIdMap(kJsonUsbVendorIdMap), Eq(std::nullopt));
}

// Verifies that ParseUsbvendorIdMap() returns std::nullopt on
// irrecoverable parse error.
TEST(PpdMetadataParserTest, ParseUsbVendorIdMapFailsGracefully) {
  EXPECT_THAT(ParseUsbVendorIdMap(kInvalidJson), Eq(std::nullopt));
}

// Verifies that ParseReverseIndex() can parse reverse index metadata.
TEST(PpdMetadataParserTest, CanParseReverseIndex) {
  constexpr std::string_view kReverseIndexJson = R"(
  {
    "reverseIndex": {
      "Die Forelle D 550d": {
        "manufacturer": "metsukabi",
        "model": "kimebe"
      },
      "Gruppe aus dem Tartarus D 583": {
        "manufacturer": "teiga",
        "model": "dahuho"
      }
    }
  }
  )";

  const auto parsed = ParseReverseIndex(kReverseIndexJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(*parsed, UnorderedElementsAre(
                           Pair(StrEq("Die Forelle D 550d"),
                                ReverseIndexLeafLike("metsukabi", "kimebe")),
                           Pair(StrEq("Gruppe aus dem Tartarus D 583"),
                                ReverseIndexLeafLike("teiga", "dahuho"))));
}

// Verifies that ParseReverseIndex() can parse reverse index metadata
// and return a partial list even when it encounters unexpected values.
TEST(PpdMetadataParserTest, CanPartiallyParseReverseIndex) {
  // Contains two unexpected values (keyed on "Dearie me" and "to go").
  // ParseReverseIndex() shall ignore these.
  constexpr std::string_view kReverseIndexJson = R"(
  {
    "reverseIndex": {
      "Dearie me": "one doesn't expect",
      "to go": "any deeper",
      "Elysium D 584": {
        "manufacturer": "nahopenu",
        "model": "sapudo"
      },
      "An den Tod D 518": {
        "manufacturer": "suwaka",
        "model": "zogegi"
      }
    }
  }
  )";

  const auto parsed = ParseReverseIndex(kReverseIndexJson);
  ASSERT_TRUE(parsed.has_value());

  EXPECT_THAT(*parsed, UnorderedElementsAre(
                           Pair(StrEq("Elysium D 584"),
                                ReverseIndexLeafLike("nahopenu", "sapudo")),
                           Pair(StrEq("An den Tod D 518"),
                                ReverseIndexLeafLike("suwaka", "zogegi"))));
}

// Verifies that ParseReverseIndex() returns std::nullopt rather than
// an empty container.
TEST(PpdMetadataParserTest, ParseReverseIndexDoesNotReturnEmptyContainer) {
  // Contains two unexpected values (keyed on "Dearie me" and "to go").
  // ParseReverseIndex() shall ignore this, but in doing so shall make the
  // returned ParsedReverseIndex empty.
  constexpr std::string_view kReverseIndexJson = R"(
  {
    "reverseIndex": {
      "Dearie me": "one doesn't expect",
      "to go": "any deeper"
    }
  }
  )";

  EXPECT_FALSE(ParseReverseIndex(kReverseIndexJson).has_value());
}

// Verifies that ParseReverseIndex() returns std::nullopt on
// irrecoverable parse error.
TEST(PpdMetadataParserTest, ParseReverseIndexFailsGracefully) {
  EXPECT_FALSE(ParseReverseIndex(kInvalidJson).has_value());
}

}  // namespace
}  // namespace chromeos