is-a.bot

Grab your own sweet-looking .i...
Log | Files | Refs | Activity | README | LICENSE

root / tests / json.test.js

json.test.js (5758B)


      1 import t from "ava";
      2 import fs from "fs-extra";
      3 import path from "path";
      4 
      5 const internalDomains = fs.readJsonSync(path.resolve("util/internal.json"));
      6 const reservedDomains = fs.readJsonSync(path.resolve("util/reserved.json"));
      7 
      8 const ignoredRootJSONFiles = ["package-lock.json", "package.json"];
      9 
     10 const requiredFields = {
     11     owner: "object",
     12     records: "object"
     13 };
     14 
     15 const optionalFields = {
     16     proxied: "boolean"
     17 };
     18 
     19 const requiredOwnerFields = {
     20     username: "string"
     21 };
     22 
     23 const optionalOwnerFields = {
     24     email: "string"
     25 };
     26 
     27 const blockedFields = [
     28     "domain",
     29     "internal",
     30     "proxy",
     31     "reserved",
     32     "services",
     33     "subdomain",
     34     "nested",
     35     "record",
     36     "redirect_config",
     37     "redirects",
     38     "redirects_config"
     39 ];
     40 
     41 const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
     42 const hostnameRegex = /^(?=.{1,253}$)(?:(?:[_a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+[a-zA-Z]{2,63}$/;
     43 
     44 const domainsPath = path.resolve("domains");
     45 const files = fs.readdirSync(domainsPath);
     46 
     47 function findDuplicateKeys(jsonString) {
     48     const duplicateKeys = new Set();
     49     const keyStack = [];
     50 
     51     const keyRegex = /"(.*?)"\s*:/g;
     52 
     53     let i = 0;
     54     while (i < jsonString.length) {
     55         const char = jsonString[i];
     56 
     57         if (char === "{") {
     58             keyStack.push({});
     59             i++;
     60             continue;
     61         }
     62 
     63         if (char === "}") {
     64             keyStack.pop();
     65             i++;
     66             continue;
     67         }
     68 
     69         keyRegex.lastIndex = i;
     70         const match = keyRegex.exec(jsonString);
     71         if (match && match.index === i && keyStack.length > 0) {
     72             const key = match[1];
     73             const currentScope = keyStack[keyStack.length - 1];
     74 
     75             if (currentScope[key]) {
     76                 duplicateKeys.add(key);
     77             } else {
     78                 currentScope[key] = true;
     79             }
     80 
     81             i = keyRegex.lastIndex;
     82         } else {
     83             i++;
     84         }
     85     }
     86 
     87     return [...duplicateKeys];
     88 }
     89 
     90 async function validateFields(t, obj, fields, file, prefix = "") {
     91     for (const key of Object.keys(fields)) {
     92         const fieldPath = prefix ? `${prefix}.${key}` : key;
     93 
     94         if (obj.hasOwnProperty(key)) {
     95             t.is(typeof obj[key], fields[key], `${file}: Field ${fieldPath} should be of type ${fields[key]}`);
     96         } else if (fields === requiredFields || fields === requiredOwnerFields) {
     97             t.true(false, `${file}: Missing required field: ${fieldPath}`);
     98         }
     99     }
    100 }
    101 
    102 async function validateFileName(t, file) {
    103     t.true(file.endsWith(".json"), `${file}: File does not have .json extension`);
    104     t.false(file.includes(".is-a.bot"), `${file}: File name should not contain .is-a.bot`);
    105     t.true(file === file.toLowerCase(), `${file}: File name should be all lowercase`);
    106     t.false(file.includes("--"), `${file}: File name should not contain consecutive hyphens`);
    107 
    108     const subdomain = file.replace(/\.json$/, "");
    109 
    110     t.regex(
    111         subdomain + ".is-a.bot",
    112         hostnameRegex,
    113         `${file}: FQDN must be 1-253 characters, can use letters, numbers, dots, and non-consecutive hyphens.`
    114     );
    115     t.false(internalDomains.includes(subdomain), `${file}: Subdomain name is registered internally`);
    116     t.false(reservedDomains.includes(subdomain), `${file}: Subdomain name is reserved`);
    117     t.true(
    118         !internalDomains.some((i) => subdomain.endsWith(`.${i}`)),
    119         `${file}: Subdomain name is registered internally`
    120     );
    121     t.true(!reservedDomains.some((r) => subdomain.endsWith(`.${r}`)), `${file}: Subdomain name is reserved`);
    122 
    123     const rootSubdomain = subdomain.split(".").pop();
    124     t.false(rootSubdomain.startsWith("_"), `${file}: Root subdomains should not start with an underscore`);
    125 }
    126 
    127 async function processFile(file, t) {
    128     const filePath = path.join(domainsPath, file);
    129     const data = await fs.readJson(filePath);
    130 
    131     validateFileName(t, file);
    132 
    133     // Check for duplicate keys
    134     const rawData = await fs.readFile(filePath, "utf8");
    135     const duplicateKeys = findDuplicateKeys(rawData);
    136     t.true(!duplicateKeys.length, `${file}: Duplicate keys found: ${duplicateKeys.join(", ")}`);
    137 
    138     // Validate fields
    139     validateFields(t, data, requiredFields, file);
    140     validateFields(t, data.owner, requiredOwnerFields, file, "owner");
    141     validateFields(t, data.owner, optionalOwnerFields, file, "owner");
    142     validateFields(t, data, optionalFields, file);
    143 
    144     if (data.owner.email) {
    145         t.regex(data.owner.email, emailRegex, `${file}: Owner email should be a valid email address`);
    146         t.false(
    147             data.owner.email.endsWith("@users.noreply.github.com"),
    148             `${file}: Owner email should not be a GitHub no-reply email`
    149         );
    150     }
    151 
    152     t.true(Object.keys(data.records).length > 0, `${file}: Missing DNS records`);
    153 
    154     for (const field of blockedFields) {
    155         t.true(!data.hasOwnProperty(field), `${file}: Disallowed field: ${field}`);
    156     }
    157 }
    158 
    159 t("JSON files should not be in the root directory", (t) => {
    160     const rootFiles = fs
    161         .readdirSync(path.resolve())
    162         .filter((file) => file.endsWith(".json") && !ignoredRootJSONFiles.includes(file));
    163     t.is(rootFiles.length, 0, "JSON files should not be in the root directory");
    164 });
    165 
    166 t("All files should be valid JSON", async (t) => {
    167     await Promise.all(
    168         files.map((file) => {
    169             return t.notThrows(() => fs.readJson(path.join(domainsPath, file)), `${file}: Invalid JSON file`);
    170         })
    171     );
    172 });
    173 
    174 t("All files should have valid file names", async (t) => {
    175     await Promise.all(files.map((file) => validateFileName(t, file)));
    176 });
    177 
    178 t("All files should have valid required and optional fields", async (t) => {
    179     await Promise.all(files.map((file) => processFile(file, t)));
    180 });
© notamitgamer • Site Built: 2026-09-05 01:53:16 UTC • git-mirror commit: c170d72 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror