summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js
blob: 51e71ff5738deebce7ce31edbd0e5707e7689804 (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
/* 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);
      }
    }
  }
}

let searchConfigSchemaV1;
let searchConfigSchema;

add_setup(async function () {
  searchConfigSchemaV1 = await IOUtils.readJSON(
    PathUtils.join(do_get_cwd().path, "search-config-schema.json")
  );
  searchConfigSchema = await IOUtils.readJSON(
    PathUtils.join(do_get_cwd().path, "search-config-v2-schema.json")
  );
});

async function checkSearchConfigValidates(schema, searchConfig) {
  disallowAdditionalProperties(schema);
  let validator = new JsonSchema.Validator(schema);

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

    let result = validator.validate(entry);
    // entry.webExtension.id supports search-config v1.
    let message = `Should validate ${
      entry.identifier ?? entry.recordType ?? entry.webExtension.id
    }`;
    if (!result.valid) {
      message += `:\n${JSON.stringify(result.errors, null, 2)}`;
    }
    Assert.ok(result.valid, message);

    // 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."
        );
      }
    }
  }
}

async function checkSearchConfigOverrideValidates(
  schema,
  searchConfigOverride
) {
  let validator = new JsonSchema.Validator(schema);

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

    let result = validator.validate(entry);

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

add_task(async function test_search_config_validates_to_schema_v1() {
  let selector = new SearchEngineSelectorOld(() => {});
  let searchConfig = await selector.getEngineConfiguration();

  await checkSearchConfigValidates(searchConfigSchemaV1, searchConfig);
});

add_task(async function test_ui_schema_valid_v1() {
  let uiSchema = await IOUtils.readJSON(
    PathUtils.join(do_get_cwd().path, "search-config-ui-schema.json")
  );

  await checkUISchemaValid(searchConfigSchemaV1, uiSchema);
});

add_task(async function test_search_config_override_validates_to_schema_v1() {
  let selector = new SearchEngineSelectorOld(() => {});
  let searchConfigOverrides = await selector.getEngineConfigurationOverrides();
  let overrideSchema = await IOUtils.readJSON(
    PathUtils.join(do_get_cwd().path, "search-config-overrides-schema.json")
  );

  await checkSearchConfigOverrideValidates(
    overrideSchema,
    searchConfigOverrides
  );
});

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

    let selector = new SearchEngineSelector(() => {});
    let searchConfig = await selector.getEngineConfiguration();

    await checkSearchConfigValidates(searchConfigSchema, searchConfig);
  }
);

add_task(
  { skip_if: () => !SearchUtils.newSearchConfigEnabled },
  async function test_ui_schema_valid() {
    let uiSchema = await IOUtils.readJSON(
      PathUtils.join(do_get_cwd().path, "search-config-v2-ui-schema.json")
    );

    await checkUISchemaValid(searchConfigSchema, uiSchema);
  }
);

add_task(
  { skip_if: () => !SearchUtils.newSearchConfigEnabled },
  async function test_search_config_override_validates_to_schema() {
    let selector = new SearchEngineSelector(() => {});
    let searchConfigOverrides =
      await selector.getEngineConfigurationOverrides();
    let overrideSchema = await IOUtils.readJSON(
      PathUtils.join(
        do_get_cwd().path,
        "search-config-overrides-v2-schema.json"
      )
    );

    await checkSearchConfigOverrideValidates(
      overrideSchema,
      searchConfigOverrides
    );
  }
);