mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
ci(release): stop on a stale dialect, check it like CI would, give prereleases their own
A stable release without a dialect of its own now ships the one on main only while that one has 90 days left; dialect 0 or less stops the run before anything is committed, unless allow_stale_dialect is ticked. Fetching and checking moved into registration-dialect.yml, which never fails its caller and also type-checks the module under the strict config and lints it with --max-warnings 0, since the bump commit skips CI. The bump and everything after it run whatever became of the dialect job. Prereleases ask the issuer for a dialect of their -pre.N version and build it into the image without committing it. The release check reads the module token by token against an allowlist of keywords, globals and member names, refuses any assignment to a property, computed access and regular expressions, and names Object, JSON and defineProperty among the forbidden. The issuer's kitchen-sink sample is gone; the tests use a hand-written dialect instead.
This commit is contained in:
@@ -1,23 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
// Checks a registration dialect the issuer handed out, before a release ships it.
|
||||
//
|
||||
// docker.yml asks the TREK services issuer for the dialect of the version it
|
||||
// releases and commits the answer as
|
||||
// server/src/nest/service-identity/registration-dialect.ts. That file then
|
||||
// runs inside every TREK server, so it is held to what a dialect is: a few
|
||||
// hashes over the inputs it is given, nothing else.
|
||||
// docker.yml (through registration-dialect.yml) asks the TREK services issuer
|
||||
// for the dialect of the version it releases and commits the answer as
|
||||
// server/src/nest/service-identity/registration-dialect.ts, without a person
|
||||
// reading it first. That file then runs inside every TREK server, so it is
|
||||
// held to what a dialect is: a few hashes over the inputs it is given, and
|
||||
// nothing that reaches past them.
|
||||
//
|
||||
// 1. Statically, before anything of it runs: the size, the header comment,
|
||||
// imports from node:crypto, node:zlib and the types module only, none of
|
||||
// the names that reach the process, the network, the file system, timers
|
||||
// or code from strings, no computed property access and no backslash (so
|
||||
// no escape can spell a forbidden name), and the version and id it must
|
||||
// carry. The generated code needs none of these.
|
||||
// 1. Statically, before anything of it runs. The size, the header comment,
|
||||
// no backslash (so no escape can spell a name), imports from node:crypto,
|
||||
// node:zlib and the types module only, and the version and id it must
|
||||
// carry. Then the code is read token by token against what it may hold,
|
||||
// not against what it may not: the few keywords the generator writes,
|
||||
// names it declares itself or imports, a handful of globals (Buffer,
|
||||
// Math, String, BigInt, Array, parseInt), and after a dot only the Buffer,
|
||||
// array, string, hash and Math members the generator uses. It may assign
|
||||
// to its own names only, never to a property (no Math.random = ...,
|
||||
// Buffer.from = ..., no defineProperty at all), read no property by a
|
||||
// computed name and hold no regular expression. A list of names that reach
|
||||
// the process, the network, timers or code from strings is checked on top.
|
||||
// 2. Then it is imported (Node strips the types) and run on the fixed inputs
|
||||
// in its SELF_CHECK: the password, the salt, scrypt over them and the
|
||||
// request must be what the issuer computed, and the request must pass
|
||||
// the rules the issuer client applies (issuer.client.ts holds the same).
|
||||
//
|
||||
// A new construct in the issuer's generator needs its name added here first,
|
||||
// in a release before the first dialect that uses it.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/ci/check-registration-dialect.mjs <file.ts> <version>
|
||||
//
|
||||
@@ -35,8 +45,9 @@ const TYPES_MODULE = './registration-dialect.types';
|
||||
const RUNTIME_MODULES = new Set(['node:crypto', 'node:zlib']);
|
||||
const IMPORT_LINE = /^import (type )?\{[\w\s,]+\} from '([^']+)';$/;
|
||||
|
||||
// Each of these reaches past the hashes: the process and its environment,
|
||||
// modules, the network, timers, or a way to run a string as code.
|
||||
// Checked on every line, on top of the allowlist below: each of these reaches
|
||||
// past the hashes, into the process and its environment, modules, the
|
||||
// network, timers, the globals' definitions, or a way to run a string as code.
|
||||
const FORBIDDEN = [
|
||||
/\bprocess\b/,
|
||||
/\brequire\b/,
|
||||
@@ -63,10 +74,126 @@ const FORBIDDEN = [
|
||||
/\b__proto__\b/,
|
||||
/\bReflect\b/,
|
||||
/\bProxy\b/,
|
||||
/\bObject\b/,
|
||||
/\bJSON\b/,
|
||||
/\bSymbol\b/,
|
||||
/\bdefineProperty\b/,
|
||||
/\bsetPrototypeOf\b/,
|
||||
/\bexport\s+default\b/,
|
||||
];
|
||||
// x[...] and x?.[...]; a type annotation's T[] is all the generated code holds.
|
||||
const COMPUTED_ACCESS = /([\w$)\]]\s*|\?\.\s*)\[(?!\])/;
|
||||
|
||||
// The keywords a dialect is written with.
|
||||
const KEYWORDS = new Set([
|
||||
'const',
|
||||
'let',
|
||||
'function',
|
||||
'return',
|
||||
'for',
|
||||
'do',
|
||||
'while',
|
||||
'export',
|
||||
'import',
|
||||
'type',
|
||||
'from',
|
||||
]);
|
||||
// Globals and type names it may name without declaring them.
|
||||
const GLOBALS = new Set(['Buffer', 'Math', 'String', 'BigInt', 'Array', 'parseInt', 'number', 'string']);
|
||||
// What it may read after a dot: the challenge's c, and the members of Buffer,
|
||||
// arrays, strings, hashes and Math the generator writes.
|
||||
const PROPERTIES = new Set([
|
||||
'c',
|
||||
'length',
|
||||
'from',
|
||||
'alloc',
|
||||
'concat',
|
||||
'subarray',
|
||||
'slice',
|
||||
'copy',
|
||||
'reverse',
|
||||
'map',
|
||||
'forEach',
|
||||
'push',
|
||||
'join',
|
||||
'padStart',
|
||||
'padEnd',
|
||||
'charAt',
|
||||
'toString',
|
||||
'toUpperCase',
|
||||
'update',
|
||||
'digest',
|
||||
'readUInt8',
|
||||
'writeUInt8',
|
||||
'readUInt32BE',
|
||||
'readUInt32LE',
|
||||
'writeUInt16BE',
|
||||
'writeUInt16LE',
|
||||
'writeUInt32BE',
|
||||
'writeUInt32LE',
|
||||
'writeBigUInt64BE',
|
||||
'writeBigUInt64LE',
|
||||
'floor',
|
||||
'sign',
|
||||
'max',
|
||||
'min',
|
||||
]);
|
||||
// Every punctuator the generator writes, longest first. `?`, `?.`, `??`, `#`
|
||||
// and `@` are not among them.
|
||||
const PUNCTUATORS = [
|
||||
'>>>=',
|
||||
'...',
|
||||
'===',
|
||||
'!==',
|
||||
'**=',
|
||||
'<<=',
|
||||
'>>=',
|
||||
'>>>',
|
||||
'=>',
|
||||
'==',
|
||||
'!=',
|
||||
'<=',
|
||||
'>=',
|
||||
'&&',
|
||||
'||',
|
||||
'++',
|
||||
'--',
|
||||
'+=',
|
||||
'-=',
|
||||
'*=',
|
||||
'/=',
|
||||
'%=',
|
||||
'&=',
|
||||
'|=',
|
||||
'^=',
|
||||
'**',
|
||||
'<<',
|
||||
'>>',
|
||||
'{',
|
||||
'}',
|
||||
'(',
|
||||
')',
|
||||
'[',
|
||||
']',
|
||||
';',
|
||||
',',
|
||||
'.',
|
||||
'<',
|
||||
'>',
|
||||
'+',
|
||||
'-',
|
||||
'*',
|
||||
'/',
|
||||
'%',
|
||||
'&',
|
||||
'|',
|
||||
'^',
|
||||
'!',
|
||||
'~',
|
||||
'=',
|
||||
':',
|
||||
];
|
||||
const ASSIGNMENTS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '>>>=', '&=', '|=', '^=']);
|
||||
// After one of these (or at the start) a `/` would open a regular expression.
|
||||
const BEFORE_EXPRESSION = new Set([...PUNCTUATORS.filter((p) => p !== ')' && p !== ']'), 'return']);
|
||||
|
||||
const CHALLENGE_PATH = /^\/v1\/register(\/[A-Za-z0-9_-]{12})?\/challenge$/;
|
||||
const REGISTER_PATH = /^\/v1\/register(\/[A-Za-z0-9_-]{12})?$/;
|
||||
@@ -78,6 +205,170 @@ const LARGEST_NONCE = 2 ** 40;
|
||||
|
||||
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// The source as tokens: { kind: 'name' | 'number' | 'string' | 'punct', value, line }.
|
||||
// Comments go, a template literal becomes a string token around the tokens of
|
||||
// each ${...} in it. Throws on anything else, a regular expression included.
|
||||
export function tokenize(source) {
|
||||
const tokens = [];
|
||||
const braces = [];
|
||||
let i = 0;
|
||||
let line = 1;
|
||||
const push = (kind, value) => tokens.push({ kind, value, line });
|
||||
const fail = (what) => {
|
||||
throw new Error(`line ${line} holds ${what}`);
|
||||
};
|
||||
const template = () => {
|
||||
// From just after a ` or a }, up to the closing ` or the next ${.
|
||||
while (i < source.length && source[i] !== '`' && !source.startsWith('${', i)) {
|
||||
if (source[i] === '\n') line += 1;
|
||||
i += 1;
|
||||
}
|
||||
if (i >= source.length) fail('a template literal that does not end');
|
||||
push('string', '`');
|
||||
if (source[i] === '`') {
|
||||
i += 1;
|
||||
} else {
|
||||
i += 2;
|
||||
braces.push('template');
|
||||
}
|
||||
};
|
||||
while (i < source.length) {
|
||||
const ch = source[i];
|
||||
if (ch === '\n') {
|
||||
line += 1;
|
||||
i += 1;
|
||||
} else if (ch === ' ') {
|
||||
i += 1;
|
||||
} else if (source.startsWith('//', i)) {
|
||||
while (i < source.length && source[i] !== '\n') i += 1;
|
||||
} else if (source.startsWith('/*', i)) {
|
||||
const end = source.indexOf('*/', i + 2);
|
||||
if (end < 0) fail('a comment that does not end');
|
||||
line += source.slice(i, end).split('\n').length - 1;
|
||||
i = end + 2;
|
||||
} else if (ch === "'" || ch === '"') {
|
||||
const end = source.indexOf(ch, i + 1);
|
||||
if (end < 0 || source.slice(i, end).includes('\n')) fail('a string that does not end');
|
||||
push('string', source.slice(i + 1, end));
|
||||
i = end + 1;
|
||||
} else if (ch === '`') {
|
||||
i += 1;
|
||||
template();
|
||||
} else if (/[A-Za-z_$]/.test(ch)) {
|
||||
const name = /^[A-Za-z_$][\w$]*/.exec(source.slice(i))[0];
|
||||
push('name', name);
|
||||
i += name.length;
|
||||
} else if (/[0-9]/.test(ch)) {
|
||||
const number = /^(0x[0-9a-fA-F]+|[0-9]+)/.exec(source.slice(i))[0];
|
||||
push('number', number);
|
||||
i += number.length;
|
||||
} else if (ch === '}' && braces.at(-1) === 'template') {
|
||||
braces.pop();
|
||||
i += 1;
|
||||
template();
|
||||
} else {
|
||||
const punct = PUNCTUATORS.find((p) => source.startsWith(p, i));
|
||||
if (!punct) fail(`the character ${JSON.stringify(ch)}`);
|
||||
if (punct === '/') {
|
||||
const before = tokens.at(-1);
|
||||
if (!before || BEFORE_EXPRESSION.has(before.value)) fail('a regular expression');
|
||||
}
|
||||
if (punct === '{') braces.push('brace');
|
||||
if (punct === '}') braces.pop();
|
||||
push('punct', punct);
|
||||
i += punct.length;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// The index of the ) that closes the ( at `open`.
|
||||
function closing(tokens, open) {
|
||||
let depth = 0;
|
||||
for (let i = open; i < tokens.length; i += 1) {
|
||||
if (tokens[i].value === '(') depth += 1;
|
||||
if (tokens[i].value === ')') depth -= 1;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
return tokens.length;
|
||||
}
|
||||
|
||||
// The names the module declares: consts, lets, functions, imports and the
|
||||
// parameters of its functions, arrows and methods.
|
||||
function declaredNames(tokens) {
|
||||
const names = new Set();
|
||||
tokens.forEach((token, i) => {
|
||||
const before = tokens[i - 1];
|
||||
if (token.kind !== 'name') return;
|
||||
if (before && ['const', 'let', 'function'].includes(before.value)) names.add(token.value);
|
||||
});
|
||||
let inImport = false;
|
||||
for (const token of tokens) {
|
||||
if (token.value === 'import') inImport = true;
|
||||
else if (token.value === 'from') inImport = false;
|
||||
else if (inImport && token.kind === 'name' && token.value !== 'type') names.add(token.value);
|
||||
}
|
||||
tokens.forEach((token, open) => {
|
||||
if (token.value !== '(') return;
|
||||
const end = closing(tokens, open);
|
||||
if (!['=>', '{', ':'].includes(tokens[end + 1]?.value)) return;
|
||||
for (let i = open + 1; i < end; i += 1) {
|
||||
const isParameter =
|
||||
tokens[i].kind === 'name' &&
|
||||
['(', ',', '{'].includes(tokens[i - 1].value) &&
|
||||
[',', ')', ':', '}', '='].includes(tokens[i + 1].value);
|
||||
if (isParameter) names.add(tokens[i].value);
|
||||
}
|
||||
});
|
||||
return names;
|
||||
}
|
||||
|
||||
// What the tokens hold that a dialect may not; empty when nothing.
|
||||
export function tokenProblems(tokens) {
|
||||
const problems = [];
|
||||
const declared = declaredNames(tokens);
|
||||
const isLocal = (i) => tokens[i]?.kind === 'name' && tokens[i - 1]?.value !== '.';
|
||||
tokens.forEach((token, i) => {
|
||||
const before = tokens[i - 1];
|
||||
const after = tokens[i + 1];
|
||||
const at = `line ${token.line}`;
|
||||
if (token.kind === 'name') {
|
||||
if (before?.value === '.') {
|
||||
if (!PROPERTIES.has(token.value)) problems.push(`${at} reads .${token.value}, which a dialect does not`);
|
||||
return;
|
||||
}
|
||||
// A key of an object literal or a parameter with its type.
|
||||
if (after?.value === ':') return;
|
||||
// A method of an object literal: name(params) { ... }.
|
||||
if (
|
||||
after?.value === '(' &&
|
||||
['{', ','].includes(before?.value) &&
|
||||
tokens[closing(tokens, i + 1) + 1]?.value === '{'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!KEYWORDS.has(token.value) && !GLOBALS.has(token.value) && !declared.has(token.value)) {
|
||||
problems.push(`${at} names ${token.value}, which it neither declares nor may use`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (token.kind !== 'punct') return;
|
||||
if (ASSIGNMENTS.has(token.value)) {
|
||||
// const out: number[] = [] has a type before the =, not a property.
|
||||
const typed = before?.value === ']' && tokens[i - 2]?.value === '[';
|
||||
if (!typed && !isLocal(i - 1)) problems.push(`${at} assigns to something other than a name of its own`);
|
||||
} else if (token.value === '++' || token.value === '--') {
|
||||
const postfix = before && (before.kind === 'name' || [')', ']'].includes(before.value));
|
||||
const target = postfix ? isLocal(i - 1) : isLocal(i + 1) && tokens[i + 2]?.value !== '.';
|
||||
if (!target) problems.push(`${at} counts something other than a name of its own`);
|
||||
} else if (token.value === '[') {
|
||||
const reads = before && (before.kind !== 'punct' || [')', ']'].includes(before.value));
|
||||
if (reads && after?.value !== ']') problems.push(`${at} reads a property by a computed name`);
|
||||
}
|
||||
});
|
||||
return [...new Set(problems)];
|
||||
}
|
||||
|
||||
// What is wrong with the source before it runs; empty when nothing is.
|
||||
export function staticProblems(source, version) {
|
||||
const problems = [];
|
||||
@@ -87,6 +378,7 @@ export function staticProblems(source, version) {
|
||||
if (!source.startsWith('/**\n')) problems.push('it does not start with its header comment');
|
||||
if (source.includes('\\')) problems.push('it holds a backslash');
|
||||
if (source.includes('\r')) problems.push('it holds a carriage return');
|
||||
if (source.includes('\t')) problems.push('it holds a tab');
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (!/^\s*(import|export\s+\*|export\s*\{)/.test(line) && !/\bfrom\s+'/.test(line)) continue;
|
||||
const match = IMPORT_LINE.exec(line);
|
||||
@@ -97,8 +389,11 @@ export function staticProblems(source, version) {
|
||||
const line = lines.findIndex((text) => pattern.test(text));
|
||||
if (line >= 0) problems.push(`line ${line + 1} names what a dialect does not need (${pattern.source})`);
|
||||
}
|
||||
const computed = lines.findIndex((text) => COMPUTED_ACCESS.test(text));
|
||||
if (computed >= 0) problems.push(`line ${computed + 1} reads a property by a computed name`);
|
||||
try {
|
||||
problems.push(...tokenProblems(tokenize(source)));
|
||||
} catch (err) {
|
||||
problems.push(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
if (!new RegExp(`^ releasedFor: '${escapeRegExp(version)}',$`, 'm').test(source)) {
|
||||
problems.push(`it is not written for ${version}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { checkDialect, staticProblems } from './check-registration-dialect.mjs';
|
||||
import { checkDialect, staticProblems, tokenize, tokenProblems } from './check-registration-dialect.mjs';
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
@@ -7,11 +7,12 @@ import { join } from 'node:path';
|
||||
import { after, describe, it } from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// The issuer's sample: every building block a generated dialect can use, for version 9.9.9.
|
||||
const SAMPLE = fileURLToPath(
|
||||
new URL('../../server/tests/fixtures/service-identity/registration-dialect.sample.ts', import.meta.url),
|
||||
// A dialect written by hand for TREK's tests, for version 9.9.9. It is none of
|
||||
// the issuer's; the issuer checks its own modules against these rules too.
|
||||
const EXAMPLE = fileURLToPath(
|
||||
new URL('../../server/tests/fixtures/service-identity/registration-dialect.example.ts', import.meta.url),
|
||||
);
|
||||
const source = readFileSync(SAMPLE, 'utf8');
|
||||
const source = readFileSync(EXAMPLE, 'utf8');
|
||||
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'registration-dialect-check-'));
|
||||
after(() => rmSync(scratch, { recursive: true, force: true }));
|
||||
@@ -25,17 +26,20 @@ function variant(text) {
|
||||
}
|
||||
|
||||
function replaced(from, to) {
|
||||
assert.ok(source.includes(from), `the sample no longer holds ${from}`);
|
||||
assert.ok(source.includes(from), `the example no longer holds ${from}`);
|
||||
return source.replace(from, to);
|
||||
}
|
||||
|
||||
// The example with `line` added in front of its exports.
|
||||
const withLine = (line) => replaced('export const registrationDialect', `${line}\nexport const registrationDialect`);
|
||||
|
||||
describe('check-registration-dialect', () => {
|
||||
it('passes the issuer sample for its own version', async () => {
|
||||
assert.deepEqual(await checkDialect(SAMPLE, '9.9.9'), []);
|
||||
it('passes the example for its own version', async () => {
|
||||
assert.deepEqual(await checkDialect(EXAMPLE, '9.9.9'), []);
|
||||
});
|
||||
|
||||
it('refuses a dialect written for another version', async () => {
|
||||
const problems = await checkDialect(SAMPLE, '9.9.10');
|
||||
const problems = await checkDialect(EXAMPLE, '9.9.10');
|
||||
assert.ok(problems.some((p) => p.includes('not written for 9.9.10')));
|
||||
});
|
||||
|
||||
@@ -51,7 +55,7 @@ describe('check-registration-dialect', () => {
|
||||
|
||||
it('refuses what reaches the process, the network or code from strings, before running any of it', async () => {
|
||||
for (const line of ['const env = process.env;', 'void fetch("https://example.org");', 'const run = eval;']) {
|
||||
const file = variant(replaced('const DIALECT =', `${line}\nconst DIALECT =`));
|
||||
const file = variant(withLine(line));
|
||||
const problems = await checkDialect(file, '9.9.9');
|
||||
assert.ok(
|
||||
problems.some((p) => p.includes('names what a dialect does not need')),
|
||||
@@ -60,27 +64,73 @@ describe('check-registration-dialect', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses a computed property name and any escape that could spell one', () => {
|
||||
const computed = replaced('const DIALECT =', "const name = BASE32['length'];\nconst DIALECT =");
|
||||
it('refuses changing a global or anything else that is not its own name', () => {
|
||||
for (const line of [
|
||||
'Math.random = () => 0;',
|
||||
'Buffer.from = Buffer.alloc;',
|
||||
'const m = Math;\nm.floor = Math.max;',
|
||||
'const b = Buffer;\nb.poolSize += 1;',
|
||||
'(Buffer).concat = Buffer.alloc;',
|
||||
'PEPPER.c++;',
|
||||
'++PEPPER.c;',
|
||||
]) {
|
||||
const problems = staticProblems(withLine(line), '9.9.9');
|
||||
assert.ok(
|
||||
problems.some((p) => p.includes('other than a name of its own') || p.includes('reads .')),
|
||||
line,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses names and members it does not know, however they are reached', () => {
|
||||
const cases = [
|
||||
['Object.defineProperty(JSON, "parse", {});', 'names what a dialect does not need'],
|
||||
['const pick = Math.random();', 'reads .random'],
|
||||
['const big = Buffer.alloc(4).buffer;', 'reads .buffer'],
|
||||
['const own = Array.isArray;', 'reads .isArray'],
|
||||
['const later = Promise.resolve(1);', 'names Promise'],
|
||||
['const date = new Date();', 'names new'],
|
||||
['delete PEPPER.c;', 'names delete'],
|
||||
['const kind = typeof PEPPER;', 'names typeof'],
|
||||
['const at = PEPPER?.length;', 'the character'],
|
||||
];
|
||||
for (const [line, expected] of cases) {
|
||||
const problems = staticProblems(withLine(line), '9.9.9');
|
||||
assert.ok(
|
||||
problems.some((p) => p.includes(expected)),
|
||||
`${line}: ${problems.join('; ')}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses a computed property name, a regular expression and any escape that could spell a name', () => {
|
||||
const computed = withLine("const name = PEPPER['length'];");
|
||||
assert.ok(staticProblems(computed, '9.9.9').some((p) => p.includes('computed name')));
|
||||
const escaped = replaced('const DIALECT =', "const name = 'pro\\u0063ess';\nconst DIALECT =");
|
||||
const pattern = withLine('const test = /process/.source;');
|
||||
assert.ok(staticProblems(pattern, '9.9.9').some((p) => p.includes('regular expression')));
|
||||
const escaped = withLine("const name = 'pro\\u0063ess';");
|
||||
assert.ok(staticProblems(escaped, '9.9.9').some((p) => p.includes('backslash')));
|
||||
});
|
||||
|
||||
it('reads the names in a template literal like any other', () => {
|
||||
assert.deepEqual(tokenProblems(tokenize('const a = 1;\nconst b = `x${a}y`;\n')), []);
|
||||
assert.ok(tokenProblems(tokenize('const b = `x${Date}y`;\n')).some((p) => p.includes('names Date')));
|
||||
});
|
||||
|
||||
it('refuses a file without its header or without a dialect id', () => {
|
||||
assert.ok(staticProblems(source.slice(source.indexOf('import')), '9.9.9').some((p) => p.includes('header')));
|
||||
const noId = replaced("const DIALECT = 'KitchenSink0';", "const DIALECT = 'short';");
|
||||
const noId = replaced("const DIALECT = 'HandWritten1';", "const DIALECT = 'short';");
|
||||
assert.ok(staticProblems(noId, '9.9.9').some((p) => p.includes('dialect id')));
|
||||
});
|
||||
|
||||
it('refuses a dialect that does not compute what its SELF_CHECK says', async () => {
|
||||
const file = variant(replaced("seal11.toString('base64url')", "seal11.toString('hex')"));
|
||||
const file = variant(replaced("seal.toString('hex')", "seal.toString('base64url')"));
|
||||
const problems = await checkDialect(file, '9.9.9');
|
||||
assert.deepEqual(problems, ['its request is not the one the issuer computed']);
|
||||
});
|
||||
|
||||
it('refuses a dialect whose request TREK would not send', async () => {
|
||||
const file = variant(replaced("'X-TREK-Ferry-Seal'", "'X-TREK-Install'"));
|
||||
const file = variant(replaced("'X-TREK-Test-Seal'", "'X-TREK-Install'"));
|
||||
const problems = await checkDialect(file, '9.9.9');
|
||||
assert.ok(problems.some((p) => p.includes('X-TREK-Install')));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user