// DateKeys, instants and rounds (lib/src/datekey.dart), as datekey.test.ts // of datekeys-ts: properties that hold for every value, on random values of // a fixed seed: dk1_ strings that read back, instants that format and parse // back to the nanosecond, and rounds whose time is the first at or after // the instant. The values and the texts of Go are in the differential // (formats_datekey.json and formats_time.json). It reads no file: it runs on // the VM and compiled to JavaScript. library; import 'dart:math'; import 'package:datekeys/src/datekey.dart'; import 'package:datekeys/src/errors.dart'; import 'package:datekeys/src/profile.dart'; import 'package:test/test.dart'; String codeOf(void Function() body) { try { body(); return 'ok'; } on DateKeysException catch (e) { return e.code.code; } } // A random int in 0..n-1 for n up to 2^53, from two halves. int below(Random r, int n) { final hi = r.nextInt(1 << 21); final lo = r.nextInt(4294967296); return (hi * 4294967296 + lo) % n; } void main() { final q = quicknet(); test('dk1_ strings of valid DateKeys read back, and invalid ones have ' 'none', () { final r = Random(7); for (var i = 0; i < 300; i++) { final d = DateKey( ['datekeys:quicknet:v1', 'a', '0:._-', 'z' * 128][i % 4], 1 + below(r, maxDateKeyRound), ); final s = compactDateKey(d); expect(s, startsWith(dk1Prefix)); expect(parseDateKey(s), d); expect('$d', s); expect( canonicalJson(d), '{"version":1,"network":"${d.profileId}","round":${d.round}}', ); } for (final d in [ const DateKey('A', 1), const DateKey('a', 0), const DateKey('a', maxDateKeyRound + 1), const DateKey('', 1), ]) { expect([canonicalJson(d), compactDateKey(d)], [null, '']); } }); test('instants order by seconds, then nanoseconds', () { Instant t(int s, int ns) => Instant(s, ns); expect(compareInstants(t(1, 0), t(1, 0)), 0); expect(compareInstants(t(1, 0), t(1, 1)), -1); expect(compareInstants(t(1, 999999999), t(2, 0)), -1); expect(compareInstants(t(2, 0), t(1, 999999999)), 1); expect(compareInstants(t(-5, 3), t(-5, 2)), 1); expect(t(3, 4), t(3, 4)); expect(t(3, 4).hashCode, t(3, 4).hashCode); expect([t(2, 0), t(1, 5), t(1, 4)]..sort(), [t(1, 4), t(1, 5), t(2, 0)]); expect(() => Instant(0, -1), throwsRangeError); expect(() => Instant(0, 1000000000), throwsRangeError); }); test('instants format and parse back, to the nanosecond', () { final r = Random(11); // From 0000-01-01 to 9999-12-31, in UTC. const first = -62167219200; const span = 253402300799 - first + 1; for (var i = 0; i < 2000; i++) { final t = Instant( first + below(r, span), [0, 1, 999999999, r.nextInt(1000000000)][i % 4], ); expect(parseRfc3339(formatRfc3339Nano(t)), t); final whole = parseRfc3339(formatRfc3339(t)); expect([whole.seconds, whole.nanos], [t.seconds, 0]); expect('$t', formatRfc3339Nano(t)); } expect(() => parseRfc3339('2023-08-23'), throwsFormatException); }); test('instants convert to and from DateTime', () { final t = DateTime.utc(2030, 1, 2, 3, 4, 5, 678); expect( Instant.fromDateTime(t), Instant(t.millisecondsSinceEpoch ~/ 1000, 678000000), ); expect(Instant.fromDateTime(t).toDateTime(), t); final before = DateTime.utc(1969, 12, 31, 23, 59, 59, 500); expect(Instant.fromDateTime(before), Instant(-1, 500000000)); expect(Instant(-1, 500000000).toDateTime(), before); }); test('a long horizon is more than 365 days ahead, to the nanosecond', () { final now = Instant(1790000000, 500); Instant at(int s, int ns) => Instant(now.seconds + s, ns); expect(longHorizonSeconds, 365 * 24 * 60 * 60); expect(isLongHorizon(at(longHorizonSeconds, 500), now), isFalse); expect(isLongHorizon(at(longHorizonSeconds, 501), now), isTrue); expect(isLongHorizon(at(1, 0), now), isFalse); }); test('the round of an instant is the first whose time is at or after it, ' 'on profiles of other periods too', () { final r = Random(13); for (final p in [ q, q.copyWith(period: 1), q.copyWith(period: 7, genesisTime: 1), q.copyWith(period: 86400, genesisTime: 253402300799 - 86400 * 10), ]) { final last = p.maxRound; final lastTime = roundTime(p, last).seconds; expect(lastTime <= maxUnixTime, isTrue); expect(lastTime + p.period > maxUnixTime, isTrue); expect(codeOf(() => roundTime(p, last + 1)), 'ERR_DATEKEY_INVALID'); expect(codeOf(() => roundTime(p, 0)), 'ERR_DATEKEY_INVALID'); for (var i = 0; i < 300; i++) { final at = Instant( p.genesisTime + below(r, lastTime - p.genesisTime + 1), [0, 0, 1, r.nextInt(1000000000)][i % 4], ); final d = resolveDateKey(p, at); final t = roundTime(p, d.round); expect(compareInstants(t, at) >= 0, isTrue); if (d.round > 1) { expect(compareInstants(roundTime(p, d.round - 1), at) < 0, isTrue); } expect(unlockAt(d, p), t); validateDateKey(d, p); } // Before the genesis and after the last round. expect( codeOf(() => resolveDateKey(p, Instant(p.genesisTime - 1, 999999999))), 'ERR_DATEKEY_INVALID', ); expect( codeOf(() => resolveDateKey(p, Instant(lastTime, 1))), 'ERR_DATEKEY_INVALID', ); } expect(unlockAt(const DateKey('other', 2), q), isNull); expect( codeOf(() => validateDateKey(const DateKey('other', 2), q)), 'ERR_PROFILE_MISMATCH', ); expect( codeOf(() => resolveDateKey(q.copyWith(period: 0), Instant(0))), 'ERR_UNKNOWN_PROFILE', ); }); }