summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js
blob: f727d60719936781511130a138b94cdc8ca10372 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
  SearchEngineSelectorOld:
    "resource://gre/modules/SearchEngineSelectorOld.sys.mjs",
});

/**
 * Checks to see if a value is an object or not.
 *
 * @param {*} value
 *   The value to check.
 * @returns {boolean}
 */
function isObject(value) {
  return value != null && typeof value == "object" && !Array.isArray(value);
}

/**
 * This function modifies the schema to prevent allowing additional properties
 * on objects. This is used to enforce that the schema contains everything that
 * we deliver via the search configuration.
 *
 * These checks are not enabled in-product, as we want to allow older versions
 * to keep working if we add new properties for whatever reason.
 *
 * @param {object} section
 *   The section to check to see if an additionalProperties flag should be added.
 */
function disallowAdditionalProperties(section) {
  // It is generally acceptable for new properties to be added to the
  // configuration as older builds will ignore them.
  //
  // As a result, we only check for new properties on nightly builds, and this
  // avoids us having to uplift schema changes. This also helps preserve the
  // schemas as documentation of "what was supported in this version".
  if (!AppConstants.NIGHTLY_BUILD) {
    return;
  }

  // If the section is a `oneOf` section, avoid the additionalProperties check.
  // Otherwise, the validator expects all properties of any `oneOf` item to be
  // present.
  if (isObject(section)) {
    if (section.properties && !("recordType" in section.properties)) {
      section.additionalProperties = false;
    }
    if ("then" in section) {
      section.then.additionalProperties = false;
    }
  }

  for (let value of Object.values(section)) {
    if (isObject(value)) {
      disallowAdditionalProperties(value);
    } else if (Array.isArray(value)) {
      for (let item of value) {
        disallowAdditionalProperties(item);
      }
    }
  }
}

/**
 * Asserts the remote setting collection validates against the schema.
 *
 * @param {object} options
 *   The options for the assertion.
 * @param {string} options.collectionName
 *   The name of the collection under validation.
 * @param {object[]} options.collectionData
 *   The collection data to validate.
 * @param {string[]} [options.ignoreFields=[]]
 *   A list of fields to ignore in the collection data, e.g. where remote
 *   settings itself adds extra fields. `schema`, `id`, and `last_modified` are
 *   always ignored.
 * @param {Function} [options.extraAssertsFn]
 *   An optional function to run additional assertions on each entry in the
 *   collection.
 * @param {Function} options.getEntryId
 *   A function to get the identifier for each entry in the collection.
 */
async function assertSearchConfigValidates({
  collectionName,
  collectionData,
  ignoreFields = [],
  extraAssertsFn,
  getEntryId,
}) {
  let schema = await IOUtils.readJSON(
    PathUtils.join(do_get_cwd().path, `${collectionName}-schema.json`)
  );

  disallowAdditionalProperties(schema);
  let validator = new JsonSchema.Validator(schema);

  for (let entry of collectionData) {
    // Records in Remote Settings contain additional properties independent of
    // the schema. Hence, we don't want to validate their presence.
    for (let field of [...ignoreFields, "schema", "id", "last_modified"]) {
      delete entry[field];
    }

    let result = validator.validate(entry);
    let message = `Should validate ${getEntryId(entry)}`;
    if (!result.valid) {
      message += `:\n${JSON.stringify(result.errors, null, 2)}`;
    }
    Assert.ok(result.valid, message);

    extraAssertsFn?.(entry);
  }
}

add_setup(async function () {
  updateAppInfo({ ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}" });
});

add_task(async function test_search_config_validates_to_schema_v1() {
  let selector = new SearchEngineSelectorOld(() => {});

  await assertSearchConfigValidates({
    collectionName: "search-config",
    collectionData: await selector.getEngineConfiguration(),
    getEntryId: entry => entry.webExtension.id,
  });
});

add_task(async function test_search_config_override_validates_to_schema_v1() {
  let selector = new SearchEngineSelectorOld(() => {});

  await assertSearchConfigValidates({
    collectionName: "search-config-overrides",
    collectionData: await selector.getEngineConfigurationOverrides(),
    getEntryId: entry => entry.telemetryId,
  });
});

add_task(
  { skip_if: () => !SearchUtils.newSearchConfigEnabled },
  async function test_search_config_validates_to_schema() {
    delete SearchUtils.newSearchConfigEnabled;
    SearchUtils.newSearchConfigEnabled = true;

    let selector = new SearchEngineSelector(() => {});

    await assertSearchConfigValidates({
      collectionName: "search-config-v2",
      collectionData: await selector.getEngineConfiguration(),
      getEntryId: entry => entry.identifier,
      extraAssertsFn: entry => {
        // All engine objects should have the base URL defined for each entry in
        // entry.base.urls.
        // Unfortunately this is difficult to enforce in the schema as it would
        // need a `required` field that works across multiple levels.
        if (entry.recordType == "engine") {
          for (let urlEntry of Object.values(entry.base.urls)) {
            Assert.ok(
              urlEntry.base,
              "Should have a base url for every URL defined on the top-level base object."
            );
          }
        }
      },
    });
  }
);

add_task(
  { skip_if: () => !SearchUtils.newSearchConfigEnabled },
  async function test_search_config_valid_partner_codes() {
    delete SearchUtils.newSearchConfigEnabled;
    SearchUtils.newSearchConfigEnabled = true;

    let selector = new SearchEngineSelector(() => {});

    for (let entry of await selector.getEngineConfiguration()) {
      if (entry.recordType == "engine") {
        for (let variant of entry.variants) {
          if (
            "partnerCode" in variant &&
            "distributions" in variant.environment
          ) {
            Assert.ok(
              variant.telemetrySuffix,
              `${entry.identifier} should have a telemetrySuffix when a distribution is specified with a partnerCode.`
            );
          }
        }
      }
    }
  }
);

add_task(
  { skip_if: () => !SearchUtils.newSearchConfigEnabled },
  async function test_search_config_override_validates_to_schema() {
    let selector = new SearchEngineSelector(() => {});

    await assertSearchConfigValidates({
      collectionName: "search-config-overrides-v2",
      collectionData: await selector.getEngineConfigurationOverrides(),
      getEntryId: entry => entry.identifier,
    });
  }
);

add_task(async function test_search_config_icons_validates_to_schema() {
  let searchIcons = RemoteSettings("search-config-icons");

  await assertSearchConfigValidates({
    collectionName: "search-config-icons",
    collectionData: await searchIcons.get(),
    ignoreFields: ["attachment"],
    getEntryId: entry => entry.engineIdentifiers[0],
  });
});

add_task(async function test_search_default_override_allowlist_validates() {
  let allowlist = RemoteSettings("search-default-override-allowlist");

  await assertSearchConfigValidates({
    collectionName: "search-default-override-allowlist",
    collectionData: await allowlist.get(),
    ignoreFields: ["attachment"],
    getEntryId: entry => entry.engineName || entry.thirdPartyId,
  });
});