You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
373 lines
20 KiB
373 lines
20 KiB
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
base64RawURL,
|
|
canonicalJSON,
|
|
compactDateKey,
|
|
formatRFC3339,
|
|
formatRFC3339Nano,
|
|
goBase64,
|
|
goBase64Decode,
|
|
MAX_ROUND,
|
|
parseDateKey,
|
|
parseRFC3339,
|
|
resolveDateKey,
|
|
resolveRFC3339,
|
|
roundTime,
|
|
unlockAt,
|
|
validateDateKey,
|
|
} from './datekey.ts';
|
|
import { maxRound, quicknet } from './profile.ts';
|
|
import { dk1, dkRound } from './testing/cborhex.ts';
|
|
import { expectCode, hx } from './testing/testdata.ts';
|
|
|
|
const te = new TextEncoder();
|
|
const q = quicknet();
|
|
const INVALID = 'ERR_DATEKEY_INVALID';
|
|
const NONCANON = 'ERR_DATEKEY_NON_CANONICAL';
|
|
|
|
describe('dk1_', () => {
|
|
it('encodes the canonical JSON and string', () => {
|
|
const d = { profileId: 'datekeys:quicknet:v1', round: 66884212 };
|
|
expect(canonicalJSON(d)).toBe('{"version":1,"network":"datekeys:quicknet:v1","round":66884212}');
|
|
expect(compactDateKey(d)).toBe(dkRound(66884212));
|
|
expect(parseDateKey(compactDateKey(d))).toEqual(d);
|
|
expect(compactDateKey({ profileId: 'a', round: MAX_ROUND })).toBe(dkRound(MAX_ROUND, 'a'));
|
|
for (const bad of [{ profileId: 'A', round: 1 }, { profileId: 'a', round: 0 }, { profileId: 'a', round: MAX_ROUND + 1 }, { profileId: 'a', round: 1.5 }]) {
|
|
expect(canonicalJSON(bad)).toBeUndefined();
|
|
expect(compactDateKey(bad)).toBe('');
|
|
}
|
|
});
|
|
|
|
it('reports decodable but non-canonical encodings as non-canonical', () => {
|
|
const json = '{"version":1,"network":"datekeys:quicknet:v1","round":1000}';
|
|
const std = Buffer.from(json).toString('base64');
|
|
const url = Buffer.from(json).toString('base64url');
|
|
for (const s of [
|
|
`dk1_${std}`,
|
|
`dk1_${url}=`,
|
|
dk1('{"round":1000,"version":1,"network":"datekeys:quicknet:v1"}'),
|
|
dk1('{"version":1e0,"network":"datekeys:quicknet:v1","round":1000}'),
|
|
dk1('{"version":1,"network":"datekeys:quicknet:v1","round":10e2}'),
|
|
dk1('{"version":1,"network":"datekeys:quicknet:v1","round":100000e-2}'),
|
|
dk1('{"version":1,"network":"datekeys:quicknet:v1","round":0.1e4}'),
|
|
dk1('{"version":1,"network":"datekeys\\u003aquicknet:v1","round":1000}'),
|
|
dk1('{"\\u0076ersion":1,"network":"datekeys:quicknet:v1","round":1000}'),
|
|
dk1('{"version":1,"network":"datekeys:quicknet:v1","round":1000} \t\r\n'),
|
|
dk1('{"version":2,"version":1,"network":"datekeys:quicknet:v1","round":1000}'),
|
|
dk1('{"version":1,"network":"datekeys:quicknet:v1","round":9007199254740991.0}'),
|
|
]) {
|
|
expectCode(() => parseDateKey(s), NONCANON);
|
|
}
|
|
});
|
|
|
|
it('rejects CR, LF and every other character outside the alphabet at step 1 (spec §19)', () => {
|
|
// Go's decoders skip CR and LF: the reference at afb44a3 reported these
|
|
// as ERR_DATEKEY_NON_CANONICAL; since 3820066 they fail step 1.
|
|
const url = Buffer.from('{"version":1,"network":"datekeys:quicknet:v1","round":1000}').toString('base64url');
|
|
for (const s of [
|
|
`dk1_${url.slice(0, 8)}\n${url.slice(8)}`,
|
|
`dk1_${url.slice(0, 8)}\r${url.slice(8)}`,
|
|
`dk1_${url}\r\n`,
|
|
`dk1_${url}\n`,
|
|
`dk1_\n${url}`,
|
|
`dk1_${url} `,
|
|
`dk1_${url}\t`,
|
|
`dk1_${url.slice(0, 8)} ${url.slice(8)}`,
|
|
]) {
|
|
expectCode(() => parseDateKey(s), INVALID, /^datekey: payload is not Base64URL: ERR_DATEKEY_INVALID$/);
|
|
}
|
|
});
|
|
|
|
it('reads JSON numbers by their exact decimal value, never as doubles (spec §19)', () => {
|
|
// 1.0000000000000001 is 1 as a double, and not 1: step 3 fails.
|
|
expectCode(() => parseDateKey(dk1('{"version":1.0000000000000001,"network":"datekeys:quicknet:v1","round":1000}')), INVALID, /unsupported version 1\.0000000000000001/);
|
|
expectCode(() => parseDateKey(dk1('{"version":1,"network":"datekeys:quicknet:v1","round":1000.0000000000001}')), INVALID, /invalid round 1000\.0000000000001/);
|
|
expectCode(() => parseDateKey(dk1('{"version":1,"network":"datekeys:quicknet:v1","round":9007199254740993}')), INVALID);
|
|
// Other spellings of the exact value pass step 3 and fail step 6.
|
|
for (const v of ['1.0', '1e0', '100e-2', '0.1e1', '1.000000000000000000000000000000']) {
|
|
expectCode(() => parseDateKey(dk1(`{"version":${v},"network":"datekeys:quicknet:v1","round":1000}`)), NONCANON, undefined, v);
|
|
}
|
|
});
|
|
|
|
it('rejects invalid UTF-8 at step 2, even in a member that a repeated name replaces (spec §19)', () => {
|
|
// Spec §19 step 2 fails on invalid UTF-8 with ERR_DATEKEY_INVALID. The
|
|
// reference replaced it with U+FFFD until 692cf87 (encoding/json), so a
|
|
// repeated name that replaced the member gave ERR_DATEKEY_NON_CANONICAL;
|
|
// both implementations now check the bytes before parsing the JSON.
|
|
const raw = (...parts: (string | number[])[]): string =>
|
|
`dk1_${Buffer.concat(parts.map((p) => (typeof p === 'string' ? Buffer.from(p) : Buffer.from(p)))).toString('base64url')}`;
|
|
const notUtf8 = /^datekey: payload is not valid UTF-8: ERR_DATEKEY_INVALID$/;
|
|
expectCode(() => parseDateKey(raw('{"version":1,"network":"', [0xff], '","network":"datekeys:quicknet:v1","round":1000}')), INVALID, notUtf8);
|
|
expectCode(() => parseDateKey(raw('{"version":1,"network":"datekeys:quicknet:v1","round":"', [0xc0, 0x80], '","round":1000}')), INVALID, notUtf8);
|
|
expectCode(() => parseDateKey(raw('{"version":1,"network":"', [0xff], '","round":1000}')), INVALID, notUtf8);
|
|
expectCode(() => parseDateKey(raw('{"version":1,"netw', [0xff], 'ork":"x","network":"datekeys:quicknet:v1","round":1000}')), INVALID, notUtf8);
|
|
});
|
|
|
|
it('rejects undecodable input and invalid fields', () => {
|
|
const cases = [
|
|
'',
|
|
'dk1',
|
|
'DK1_' + dkRound(1).slice(4),
|
|
'dk1_!!!',
|
|
'dk1_' + 'A'.repeat(253),
|
|
'dk1_é',
|
|
dk1('\u{feff}{"version":1,"network":"a","round":1}'),
|
|
dk1('{"version":1,"network":"a","round":1}x'),
|
|
dk1('{"version":1,"network":"a","round":1}{}'),
|
|
dk1('{"version":1,"network":"a","round":1,}'),
|
|
dk1('{"version":1 "network":"a","round":1}'),
|
|
dk1('{"version":1,"network":"a"}'),
|
|
dk1('{"version":1,"network":"a","round":1,"x":null}'),
|
|
dk1('{"version":2,"network":"a","round":1}'),
|
|
dk1('{"version":"1","network":"a","round":1}'),
|
|
dk1('{"version":1,"network":"A","round":1}'),
|
|
dk1('{"version":1,"network":1,"round":1}'),
|
|
dk1('{"version":1,"network":"a\\ud800","round":1}'),
|
|
dk1('{"version":1,"network":"a","round":0}'),
|
|
dk1('{"version":1,"network":"a","round":-1}'),
|
|
dk1('{"version":1,"network":"a","round":-0}'),
|
|
dk1('{"version":1,"network":"a","round":1.5}'),
|
|
dk1('{"version":1,"network":"a","round":9007199254740992}'),
|
|
dk1('{"version":1,"network":"a","round":18446744073709551616}'),
|
|
dk1('{"version":1,"network":"a","round":1e32768}'),
|
|
dk1('{"version":1,"network":"a","round":1e21}'),
|
|
dk1('{"version":1,"network":"a","round":01}'),
|
|
dk1('{"version":1,"network":"a","round":1.}'),
|
|
dk1('{"version":1,"network":"a","round":+1}'),
|
|
dk1('{"version":1,"network":"a","round":1e}'),
|
|
dk1('{"version":1,"network":"a","round":[1]}'),
|
|
dk1('{"version":1,"network":"a","round":tru}'),
|
|
dk1('{"version":1,"network":"a","round":1,"x":"\\x"}'),
|
|
dk1('{"version":1,"network":"a","round":1,"x":"\u0001"}'),
|
|
dk1('{"version":1,"network":"a","round":1,"x":"\\u12"}'),
|
|
dk1('{"version":1,"network":"a","round":1,"x":"'),
|
|
dk1('{"version":1,"network":"a","round":1'),
|
|
dk1('{"version":1,"network":"a","round":1,1:2}'),
|
|
dk1('{"version":1,"network":"a","round":1,"x"}'),
|
|
dk1('[1]'),
|
|
dk1('{}'),
|
|
dk1('{ }'),
|
|
dk1('{"version":1,"network":"a","round":-a}'),
|
|
dk1('{"version":1,"network":"a","round":1e+a}'),
|
|
dk1('null'),
|
|
dk1('"x"'),
|
|
dk1(''),
|
|
dk1('{"version":1,"network":"a","round":1,"a":' + '['.repeat(40) + ']'.repeat(40) + '}'),
|
|
dk1('{"version":1,"network":"a","round":1,"a":{"b":[true,false,null,{"c":-1.5e+3}]}}'),
|
|
];
|
|
for (const s of cases) expectCode(() => parseDateKey(s), INVALID);
|
|
});
|
|
|
|
it('bounds the input at 256 UTF-8 bytes, as Go', () => {
|
|
const longer = /^datekey: input longer than 256 bytes: ERR_DATEKEY_INVALID$/;
|
|
// The texts are those of Go's datekey.Parse on the same UTF-8 bytes.
|
|
expectCode(() => parseDateKey('dk1_' + 'A'.repeat(252)), INVALID, /^datekey: payload is not a JSON object: /);
|
|
expectCode(() => parseDateKey('dk1_' + 'A'.repeat(253)), INVALID, longer);
|
|
expectCode(() => parseDateKey('dk1_' + 'é'.repeat(126)), INVALID, /^datekey: payload is not Base64URL: /);
|
|
expectCode(() => parseDateKey('dk1_' + 'é'.repeat(127)), INVALID, longer);
|
|
// A lone surrogate counts as the 3 bytes of U+FFFD, its UTF-8 encoding.
|
|
expectCode(() => parseDateKey('dk1_' + '\ud800'.repeat(84)), INVALID, /^datekey: payload is not Base64URL: /);
|
|
expectCode(() => parseDateKey('dk1_' + '\ud800'.repeat(85)), INVALID, longer);
|
|
// Very long input is rejected on its length alone.
|
|
expectCode(() => parseDateKey('dk1_' + 'A'.repeat(10_000_000)), INVALID, longer);
|
|
});
|
|
|
|
it('decodes JSON strings as Go does', () => {
|
|
// Invalid UTF-8 bytes fail step 2 (spec §19); valid runes and escapes,
|
|
// where an unpaired surrogate escape becomes U+FFFD as in Go, reach step 3,
|
|
// where the extra field "x" fails.
|
|
const withBytes = (bytes: number[]): string =>
|
|
'dk1_' + Buffer.from([...te.encode('{"version":1,"network":"datekeys:quicknet:v1","round":1,"x":"'), ...bytes, 0x22, 0x7d]).toString('base64url');
|
|
for (const bytes of [[0xff], [0xe2, 0x82], [0xc0, 0x80], [0xed, 0xa0, 0x80], [0xf4, 0x90, 0x80, 0x80], [0xf0, 0x9f, 0x98, 0x80], [0xc3, 0xa9], [0xe2, 0x82, 0xac]]) {
|
|
expectCode(() => parseDateKey(withBytes(bytes)), INVALID);
|
|
}
|
|
expectCode(() => parseDateKey(dk1('{"version":1,"network":"datekeys:quicknet:v1","round":1,"x":"\\ud83d\\ude00\\n\\t\\b\\f\\r\\"\\\\"}')), INVALID);
|
|
expectCode(() => parseDateKey(dk1('{"version":1,"network":"datekeys:quicknet:v1","round":1,"x":"\\udc00\\ud800"}')), INVALID);
|
|
});
|
|
});
|
|
|
|
describe('Go base64', () => {
|
|
const dec = (s: string, url: boolean, padded: boolean, strict: boolean): string | undefined => {
|
|
const b = goBase64Decode(te.encode(s), url, padded, strict);
|
|
return b === undefined ? undefined : hx(b);
|
|
};
|
|
|
|
it('decodes the four variants', () => {
|
|
expect(dec('', true, false, false)).toBe('');
|
|
expect(dec('_-8', true, false, false)).toBe('ffef');
|
|
expect(dec('/+8', false, false, false)).toBe('ffef');
|
|
expect(dec('/+8=', false, true, false)).toBe('ffef');
|
|
expect(dec('QQ==', false, true, false)).toBe('41');
|
|
expect(dec('QUJD', false, true, false)).toBe('414243');
|
|
expect(dec('QQ', false, false, false)).toBe('41');
|
|
});
|
|
|
|
it('skips CR and LF anywhere', () => {
|
|
expect(dec('Q\nU\rJ\nD\n', false, false, false)).toBe('414243');
|
|
expect(dec('QQ=\n=\n', false, true, false)).toBe('41');
|
|
expect(dec('QUI\n=', false, true, false)).toBe('4142');
|
|
});
|
|
|
|
it('rejects what Go rejects', () => {
|
|
expect(dec('Q', false, false, false)).toBeUndefined();
|
|
expect(dec('QQ', false, true, false)).toBeUndefined();
|
|
expect(dec('QQ=', false, true, false)).toBeUndefined();
|
|
expect(dec('QQ=x', false, true, false)).toBeUndefined();
|
|
expect(dec('Q===', false, true, false)).toBeUndefined();
|
|
expect(dec('=QQQ', false, true, false)).toBeUndefined();
|
|
expect(dec('QQ==QQ==', false, true, false)).toBeUndefined();
|
|
expect(dec('QQ==\nx', false, true, false)).toBeUndefined();
|
|
expect(dec('QQ=', false, false, false)).toBeUndefined();
|
|
expect(dec('Q-', false, false, false)).toBeUndefined();
|
|
expect(dec('Q+', true, false, false)).toBeUndefined();
|
|
expect(dec('Q é', true, false, false)).toBeUndefined();
|
|
});
|
|
|
|
it('checks the unused bits only when strict', () => {
|
|
expect(dec('QR', false, false, false)).toBe('41');
|
|
expect(dec('QR', false, false, true)).toBeUndefined();
|
|
expect(dec('QUI', false, false, true)).toBe('4142');
|
|
expect(dec('QUJ', false, false, true)).toBeUndefined();
|
|
expect(dec('QUJ', false, false, false)).toBe('4142');
|
|
expect(dec('QUK', false, false, true)).toBeUndefined();
|
|
expect(dec('QUK', false, false, false)).toBe('4142');
|
|
});
|
|
|
|
it('reports the offsets of Go CorruptInputError', () => {
|
|
// Offsets printed by Go 1.26 for RawStdEncoding.Strict() and StdEncoding.
|
|
const at = (s: string, padded: boolean, strict: boolean): number | string => {
|
|
const r = goBase64(te.encode(s), false, padded, strict);
|
|
return typeof r === 'number' ? r : hx(r);
|
|
};
|
|
const cases: [string, number | string, number | string][] = [
|
|
['QQ', '41', 0],
|
|
['Q', 0, 0],
|
|
['QQ=', 2, 3],
|
|
['QQ=x', 2, 2],
|
|
['Q===', 1, 1],
|
|
['QQ==\nx', 2, 5],
|
|
['QUK', 2, 0],
|
|
['QR', 0, 0],
|
|
['Q-', 1, 1],
|
|
['QUJD=', 4, 4],
|
|
['QUJDRA==', 6, '41424344'],
|
|
['QUJDR', 4, 4],
|
|
['QU\nJDRA=\n=\nx', 7, 11],
|
|
];
|
|
for (const [s, rawStrict, std] of cases) {
|
|
expect(at(s, false, true), `${JSON.stringify(s)} raw strict`).toBe(rawStrict);
|
|
expect(at(s, true, false), `${JSON.stringify(s)} std`).toBe(std);
|
|
}
|
|
});
|
|
|
|
it('describes invalid JSON fields as Go %v', () => {
|
|
const msg = (json: string): string => {
|
|
try {
|
|
parseDateKey(dk1(json));
|
|
} catch (err) {
|
|
return (err as Error).message;
|
|
}
|
|
return '';
|
|
};
|
|
expect(msg('{"version":[1,"a",true,null,{"b":2,"a":[]}],"network":"a","round":1}')).toBe(
|
|
'datekey: unsupported version [1 a true <nil> map[a:[] b:2]]: ERR_DATEKEY_INVALID',
|
|
);
|
|
expect(msg('{"version":1,"network":{},"round":1}')).toBe('datekey: invalid network map[]: ERR_DATEKEY_INVALID');
|
|
expect(msg('{"version":1,"network":"a","round":false}')).toBe('datekey: invalid round false: ERR_DATEKEY_INVALID');
|
|
expect(msg('{"version":1,"network":"a","x":1}')).toBe('datekey: invalid round <nil>: ERR_DATEKEY_INVALID');
|
|
expect(msg('{"version":1,"network":"A","round":1}')).toBe('datekey: invalid network A: ERR_DATEKEY_INVALID');
|
|
expect(msg('{"version":1e0,"network":"a","round":1}')).toBe(`datekey: not the canonical encoding ${dkRound(1, 'a')}: ERR_DATEKEY_NON_CANONICAL`);
|
|
expectCode(() => parseDateKey('x'), INVALID, /^datekey: missing "dk1_" prefix: ERR_DATEKEY_INVALID$/);
|
|
});
|
|
|
|
it('encodes unpadded Base64URL', () => {
|
|
for (const n of [0, 1, 2, 3, 4, 5, 64]) {
|
|
const b = Uint8Array.from({ length: n }, (_, i) => (i * 37 + 250) & 0xff);
|
|
expect(base64RawURL(b)).toBe(Buffer.from(b).toString('base64url'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('RFC 3339', () => {
|
|
it('parses with nanosecond precision', () => {
|
|
expect(parseRFC3339('2023-08-23T15:09:27Z')).toEqual({ seconds: 1692803367, nanos: 0 });
|
|
expect(parseRFC3339('2023-08-23T15:09:27.000000001Z')).toEqual({ seconds: 1692803367, nanos: 1 });
|
|
expect(parseRFC3339('2023-08-23T15:09:27.5Z')).toEqual({ seconds: 1692803367, nanos: 500000000 });
|
|
expect(parseRFC3339('2023-08-23T15:09:27.1234567899Z')).toEqual({ seconds: 1692803367, nanos: 123456789 });
|
|
expect(parseRFC3339('2026-10-22T19:00:00.001+02:00')).toEqual({ seconds: 1792688400, nanos: 1000000 });
|
|
expect(parseRFC3339('2023-08-23T15:09:27-01:30').seconds).toBe(1692803367 + 5400);
|
|
expect(parseRFC3339('1969-12-31T23:59:59.5Z')).toEqual({ seconds: -1, nanos: 500000000 });
|
|
expect(parseRFC3339('0000-01-01T00:00:00Z').seconds).toBe(-62167219200);
|
|
expect(parseRFC3339('9999-12-31T23:59:59Z').seconds).toBe(253402300799);
|
|
expect(parseRFC3339('2024-02-29T00:00:00Z').seconds).toBe(1709164800);
|
|
expect(parseRFC3339('2000-02-29T00:00:00Z').seconds).toBe(951782400);
|
|
});
|
|
|
|
it('accepts the forms of Go time.Parse general layout path', () => {
|
|
expect(parseRFC3339('2023-08-23T5:09:27Z').seconds).toBe(1692767367);
|
|
expect(parseRFC3339('2023-08-23T15:09:27,5Z')).toEqual({ seconds: 1692803367, nanos: 500000000 });
|
|
expect(parseRFC3339('2023-08-23T15:09:27+24:00').seconds).toBe(1692803367 - 86400);
|
|
expect(parseRFC3339('2023-08-23T15:09:27-23:60').seconds).toBe(1692803367 + 86400);
|
|
expect(parseRFC3339('2023-08-23T9:09:27,000000001+24:00')).toEqual({ seconds: 1692781767 - 86400, nanos: 1 });
|
|
});
|
|
|
|
it('rejects what Go rejects', () => {
|
|
for (const s of [
|
|
'', 'x', '2023-08-23', '2023-08-23T15:09Z', '2023-08-23t15:09:27Z', '2023-08-23T15:09:27z', '2023-08-23 15:09:27Z',
|
|
'2023-8-23T15:09:27Z', '2023-08-23T05:9:27Z', '2023-08-23T15:09:27.Z', '2023-08-23T15:09:27.5', '2023-08-23T15:09:27+0200',
|
|
'2023-08-23T15:09:27+02', '2023-08-23T15:09:27+02:00:00', '2023-08-23T24:00:00Z', '2023-08-23T23:60:00Z', '2023-08-23T23:59:60Z',
|
|
'2023-02-29T00:00:00Z', '2100-02-29T00:00:00Z', '2023-04-31T00:00:00Z', '2023-13-01T00:00:00Z', '2023-00-01T00:00:00Z',
|
|
'2023-01-00T00:00:00Z', '10000-01-01T00:00:00Z', '+2023-08-23T15:09:27Z', ' 2023-08-23T15:09:27Z', '2023-08-23T15:09:27Z ',
|
|
'2023-08-23T15:09:27ZZ', '2023-08-23T15:09:27Z+01:00', '2023-08-23T15:09:27+25:00', '2023-08-23T15:09:27+23:61', '2023-08-23T15:09:27+1:00',
|
|
'2023-08-23T15:09:27+01:0a', '2023-08-23T15:09:27*01:00', '2023-08-23T15:09:27.1,2Z', '\u{ff12}023-08-23T15:09:27Z', '2023-08-23T15:09:27\u{a0}',
|
|
'2023-08-23T1:9:27Z', '2023-08-23T123:09:27Z', '2023-08-23T15:09:27,Z', '2023-08-2aT15:09:27Z', '20a3-08-23T15:09:27Z', '2023-08-23X15:09:27Z',
|
|
'2023-08-23T15-09:27Z', '2023-08-23T15:09-27Z', '2023/08-23T15:09:27Z', '2023-08/23T15:09:27Z',
|
|
]) {
|
|
expect(() => parseRFC3339(s), s).toThrow(TypeError);
|
|
}
|
|
});
|
|
|
|
it('formats UTC instants', () => {
|
|
expect(formatRFC3339({ seconds: 1692803367, nanos: 0 })).toBe('2023-08-23T15:09:27Z');
|
|
expect(formatRFC3339({ seconds: -1, nanos: 0 })).toBe('1969-12-31T23:59:59Z');
|
|
expect(formatRFC3339({ seconds: 253402300799, nanos: 0 })).toBe('9999-12-31T23:59:59Z');
|
|
expect(formatRFC3339({ seconds: 951782400, nanos: 0 })).toBe('2000-02-29T00:00:00Z');
|
|
expect(formatRFC3339Nano({ seconds: 1692803367, nanos: 500000000 })).toBe('2023-08-23T15:09:27.5Z');
|
|
expect(formatRFC3339Nano({ seconds: 1692803367, nanos: 1 })).toBe('2023-08-23T15:09:27.000000001Z');
|
|
expect(formatRFC3339Nano({ seconds: 1692803367, nanos: 0 })).toBe('2023-08-23T15:09:27Z');
|
|
// Years before 1 as Go's time.Format: the sign, then at least 4 digits.
|
|
expect(formatRFC3339({ seconds: -62135596800, nanos: 0 })).toBe('0001-01-01T00:00:00Z');
|
|
expect(formatRFC3339({ seconds: -62167219200, nanos: 0 })).toBe('0000-01-01T00:00:00Z');
|
|
expect(formatRFC3339({ seconds: -62167219201, nanos: 0 })).toBe('-0001-12-31T23:59:59Z');
|
|
expect(formatRFC3339({ seconds: -62167305600, nanos: 0 })).toBe('-0001-12-31T00:00:00Z');
|
|
expect(formatRFC3339Nano({ seconds: -62167305600, nanos: 500000000 })).toBe('-0001-12-31T00:00:00.5Z');
|
|
expect(formatRFC3339({ seconds: -377705116800, nanos: 0 })).toBe('-9999-01-01T00:00:00Z');
|
|
});
|
|
});
|
|
|
|
describe('rounds', () => {
|
|
it('resolves, validates and times rounds', () => {
|
|
expect(resolveRFC3339(q, '2030-01-01T00:00:00Z').round).toBe(66884212);
|
|
expect(roundTime(q, 1)).toEqual({ seconds: q.genesisTime, nanos: 0 });
|
|
expect(formatRFC3339(roundTime(q, maxRound(q)))).toBe('9999-12-31T23:59:57Z');
|
|
expectCode(() => roundTime(q, 0), INVALID);
|
|
expectCode(() => roundTime(q, maxRound(q) + 1), INVALID, /round 83903165812 outside 1\.\.83903165811 of datekeys:quicknet:v1/);
|
|
expect(unlockAt({ profileId: q.id, round: 2 }, q)).toEqual({ seconds: q.genesisTime + 3, nanos: 0 });
|
|
expect(unlockAt({ profileId: 'other', round: 2 }, q)).toBeUndefined();
|
|
expectCode(() => validateDateKey({ profileId: 'other', round: 2 }, q), 'ERR_PROFILE_MISMATCH');
|
|
expectCode(() => validateDateKey({ profileId: q.id, round: maxRound(q) + 1 }, q), INVALID);
|
|
expectCode(() => resolveDateKey(q, { seconds: q.genesisTime - 1, nanos: 999999999 }), INVALID, /before the genesis/);
|
|
expectCode(() => resolveDateKey(q, { seconds: 253402300800, nanos: 0 }), INVALID, /after 9999-12-31T23:59:59Z/);
|
|
// Go's time.Parse moves these into year -1; the texts are Go's.
|
|
expectCode(
|
|
() => resolveRFC3339(q, '0000-01-01T00:00:00+24:00'),
|
|
INVALID,
|
|
/^datekey: -0001-12-31T00:00:00Z is before the genesis of datekeys:quicknet:v1: ERR_DATEKEY_INVALID$/,
|
|
);
|
|
expectCode(() => resolveRFC3339(q, '0000-01-01T00:00:00+23:59'), INVALID, /^datekey: -0001-12-31T00:01:00Z is before the genesis/);
|
|
expectCode(() => resolveRFC3339(q, '0000-01-01T00:00:00.5+01:60'), INVALID, /^datekey: -0001-12-31T22:00:00\.5Z is before the genesis/);
|
|
expectCode(() => resolveDateKey({ ...q, period: 0 }, { seconds: q.genesisTime, nanos: 0 }), 'ERR_UNKNOWN_PROFILE');
|
|
expectCode(() => resolveDateKey({ ...q, period: 1.5 }, { seconds: q.genesisTime, nanos: 0 }), 'ERR_UNKNOWN_PROFILE');
|
|
});
|
|
});
|