aboutsummaryrefslogtreecommitdiff
path: root/test/storage/StorageSupport.mc
diff options
context:
space:
mode:
Diffstat (limited to 'test/storage/StorageSupport.mc')
-rw-r--r--test/storage/StorageSupport.mc250
1 files changed, 250 insertions, 0 deletions
diff --git a/test/storage/StorageSupport.mc b/test/storage/StorageSupport.mc
new file mode 100644
index 0000000..51a0c7c
--- /dev/null
+++ b/test/storage/StorageSupport.mc
@@ -0,0 +1,250 @@
+// Author: cirakg & claude ai
+// Summary: Test doubles for storage: an in-memory store with a size limit, a hand-set clock, and fake entries.
+//
+// MemoryBackend copies values on the way in and out, as Application.Storage
+// does, so a test can't pass by keeping a reference to stored data. raw()
+// gives tests the stored object itself, for tampering.
+
+import Toybox.Application;
+import Toybox.Lang;
+import Toybox.Test;
+
+(:test)
+class MemoryBackend extends Persist.Backend {
+
+ // Total size limit in (approximate) bytes; setValue throws above it.
+ var capacity as Number = 1000000;
+ private var _data as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType> = {} as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>;
+
+ function initialize() {
+ Backend.initialize();
+ }
+
+ function getValue(key as String) as Application.Storage.ValueType {
+ return MemoryBackend.copyOf(_data[key]);
+ }
+
+ function setValue(key as String, value as Application.Storage.ValueType) as Void {
+ var hadKey = _data.hasKey(key);
+ var old = _data[key];
+ _data[key] = MemoryBackend.copyOf(value);
+ if (usedBytes() > capacity) {
+ if (hadKey) {
+ _data[key] = old;
+ } else {
+ _data.remove(key);
+ }
+ throw new Lang.StorageFullException("Storage full");
+ }
+ }
+
+ function deleteValue(key as String) as Void {
+ _data.remove(key);
+ }
+
+ // The stored object itself (not a copy), for tampering in tests.
+ function raw(key as String) as Application.Storage.ValueType {
+ return _data[key];
+ }
+
+ function keys() as Array<String> {
+ return _data.keys() as Array<String>;
+ }
+
+ function usedBytes() as Number {
+ var total = 0;
+ var keys = _data.keys();
+ for (var i = 0; i < keys.size(); i++) {
+ var key = keys[i] as String;
+ total += key.length() + MemoryBackend.sizeOf(_data[key]);
+ }
+ return total;
+ }
+
+ // Rough serialized size, enough to make "store full" reachable.
+ static function sizeOf(value as Application.Storage.ValueType) as Number {
+ if (value instanceof ByteArray) {
+ return value.size();
+ }
+ if (value instanceof String) {
+ return value.length();
+ }
+ if (value instanceof Array) {
+ var total = 0;
+ for (var i = 0; i < value.size(); i++) {
+ total += sizeOf(value[i] as Application.Storage.ValueType);
+ }
+ return total;
+ }
+ if (value instanceof Dictionary) {
+ var total = 0;
+ var keys = value.keys();
+ for (var i = 0; i < keys.size(); i++) {
+ total += sizeOf(keys[i] as Application.Storage.ValueType) + sizeOf(value[keys[i]] as Application.Storage.ValueType);
+ }
+ return total;
+ }
+ return 8;
+ }
+
+ // Deep copy of arrays, dictionaries and byte arrays.
+ static function copyOf(value as Application.Storage.ValueType) as Application.Storage.ValueType {
+ if (value instanceof ByteArray) {
+ return Bytes.copy(value);
+ }
+ if (value instanceof Array) {
+ var out = [] as Array<Application.Storage.ValueType>;
+ for (var i = 0; i < value.size(); i++) {
+ out.add(copyOf(value[i] as Application.Storage.ValueType));
+ }
+ return out;
+ }
+ if (value instanceof Dictionary) {
+ var out = {} as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>;
+ var keys = value.keys();
+ for (var i = 0; i < keys.size(); i++) {
+ out[keys[i] as Application.Storage.KeyType] = copyOf(value[keys[i]] as Application.Storage.ValueType);
+ }
+ return out;
+ }
+ return value;
+ }
+}
+
+// A clock the test sets by hand.
+(:test)
+class FakeTime extends TimeSource {
+
+ var trusted as Long? = 1800000000l;
+ var uptime as Number = 1000;
+
+ function initialize() {
+ TimeSource.initialize();
+ }
+
+ function trustedUnix() as Long? {
+ return trusted;
+ }
+
+ function uptimeMs() as Number {
+ return uptime;
+ }
+
+ // Advance both clocks (the trusted one only when it is synced).
+ function advance(seconds as Number) as Void {
+ uptime += seconds * 1000;
+ var t = trusted;
+ if (t != null) {
+ trusted = t + seconds;
+ }
+ }
+}
+
+(:test)
+module StorageFixtures {
+
+ const PIN = "246810";
+ const OTHER_PIN = "135799";
+ const ITERATIONS = 2000; // Kdf.MIN_ITERATIONS: the cheapest allowed
+
+ // A fake TOTP entry (the secret is the RFC 4226 test key, not a real one).
+ function totp(name as String) as Entry {
+ var e = new Entry();
+ e.kind = Entry.KIND_OTP;
+ e.type = Entry.OTP_TOTP;
+ e.name = name;
+ e.account = "alice@example.com";
+ e.group = "Work";
+ e.algorithm = Hashes.SHA256;
+ e.digits = 8;
+ e.period = 60;
+ e.secret = Bytes.fromUtf8("12345678901234567890");
+ e.created = 1700000000l;
+ return e;
+ }
+
+ function hotp(name as String, counter as Long) as Entry {
+ var e = totp(name);
+ e.type = Entry.OTP_HOTP;
+ e.algorithm = Hashes.SHA1;
+ e.digits = 6;
+ e.period = 30;
+ e.counter = counter;
+ return e;
+ }
+
+ function steam(name as String) as Entry {
+ var e = totp(name);
+ e.type = Entry.OTP_STEAM;
+ e.algorithm = Hashes.SHA1;
+ e.digits = 6;
+ e.period = 30;
+ return e;
+ }
+
+ function code(name as String) as Entry {
+ var e = new Entry();
+ e.kind = Entry.KIND_CODE;
+ e.type = 3;
+ e.name = name;
+ e.payload = Bytes.fromUtf8("CARD-0000-1111-2222 éß");
+ e.options = 0x102;
+ e.source = Entry.SOURCE_IMPORT;
+ e.favorite = true;
+ return e;
+ }
+
+ function pin(text as String) as ByteArray {
+ return Bytes.fromUtf8(text);
+ }
+
+ // Set the first PIN (leaves the session unlocked).
+ function setPin(vault as Vault, text as String) as Void {
+ var job = vault.beginSetPin(pin(text), ITERATIONS);
+ Jobs.runToCompletion(job, Kdf.ITERATIONS_PER_STEP);
+ vault.finishSetPin(job);
+ }
+
+ // One unlock attempt; true when the PIN was right.
+ function unlock(vault as Vault, text as String) as Boolean {
+ var job = vault.beginUnlock(pin(text));
+ Jobs.runToCompletion(job, Kdf.ITERATIONS_PER_STEP);
+ return vault.finishUnlock(job);
+ }
+
+ // True when the two entries hold the same fields (secrets included).
+ function same(logger as Test.Logger, a as Entry, b as Entry) as Boolean {
+ var problems = [] as Array<String>;
+ if (!a.id.equals(b.id)) { problems.add("id"); }
+ if (a.kind != b.kind || a.type != b.type) { problems.add("kind/type"); }
+ if (!a.name.equals(b.name) || !a.account.equals(b.account) || !a.group.equals(b.group)) { problems.add("names"); }
+ if (a.locked != b.locked || a.source != b.source || a.favorite != b.favorite) { problems.add("flags"); }
+ if (a.counter != b.counter || a.created != b.created) { problems.add("counter/created"); }
+ if (a.namesEncrypted != b.namesEncrypted) { problems.add("namesEncrypted"); }
+ if (a.algorithm != b.algorithm || a.digits != b.digits || a.period != b.period || a.options != b.options) {
+ problems.add("parameters");
+ }
+ if (!sameOptional(a.secret, b.secret) || !sameOptional(a.payload, b.payload)) { problems.add("secret/payload"); }
+ if (problems.size() > 0) {
+ logger.error("entries differ in " + problems.toString());
+ return false;
+ }
+ return true;
+ }
+
+ function sameOptional(a as ByteArray?, b as ByteArray?) as Boolean {
+ if (a == null || b == null) {
+ return a == null && b == null;
+ }
+ return Bytes.equal(a, b);
+ }
+
+ function allZero(data as ByteArray) as Boolean {
+ for (var i = 0; i < data.size(); i++) {
+ if (data[i] != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+}