summaryrefslogtreecommitdiff
path: root/test/settings/SettingsJobTest.mc
blob: 07dbc643f62e441400b7d81ed5c94462cb04c44d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// Author:  cirakg & claude ai
// Summary: Tests for the whole settings run: README order, the import through its form, the About fields and "Encrypt names".

import Toybox.Application;
import Toybox.Lang;
import Toybox.Test;

// All forms at once are applied in README 10.3 order: Import, Add OTP,
// Add code, Manage.
(:test)
function settingsAppliesFormsInOrder(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var first = vault.ids()[0];
    var s = SettingsFixtures.store();
    SettingsFixtures.manage(s, first, ManageAction.DELETE);
    SettingsFixtures.code(s, Codes.QR, "Member", "https://example.com/m/42");
    SettingsFixtures.otp(s, Entry.OTP_TOTP, "Mail", "bob", AegisFixtures.SECRET_2);
    s.set("impText1", ImporterFixtures.uri("Proton", "zoe", AegisFixtures.SECRET_3));
    s.set("impApply", true);
    var job = SettingsFixtures.run(vault, s);
    var lines = job.lines;
    var order = ["Import: Imported 1", "Add OTP: Added", "Add code: Added", "Manage: Deleted " + first];
    var at = 0;
    for (var i = 0; i < lines.size() && at < order.size(); i++) {
        if (lines[i].find(order[at] as String) == 0) {
            at++;
        }
    }
    if (at != order.size() || job.forms != 4) {
        logger.error(lines.toString());
        return false;
    }
    // 3 fixtures + 3 added - 1 deleted.
    return vault.ids().size() == 5 && SettingsFixtures.text(s, "impText1").equals("") && !Forms.flag(s, "impApply")
        && SettingsFixtures.text(s, "impStatus").equals("Imported 1");
}

// The import form keeps its M5 behaviour: a repeat is ignored, a cancel
// leaves the text.
(:test)
function settingsImportFormIsAppliedOnce(logger as Test.Logger) as Boolean {
    var vault = SettingsFixtures.vault();
    var s = SettingsFixtures.store();
    var uri = ImporterFixtures.uri("Proton", "zoe", AegisFixtures.SECRET_3);
    s.set("impText1", uri);
    s.set("impApply", true);
    SettingsFixtures.run(vault, s);
    s.set("impText1", uri);
    s.set("impApply", true);
    SettingsFixtures.run(vault, s);
    var repeated = SettingsFixtures.text(s, "impStatus").equals("Already imported") && vault.ids().size() == 1;
    s.set("impText1", "partial");
    s.set("impMode", Importer.MODE_CANCEL);
    s.set("impApply", true);
    SettingsFixtures.run(vault, s);
    return repeated && SettingsFixtures.text(s, "impStatus").equals("Waiting import cancelled")
        && SettingsFixtures.text(s, "impText1").equals("partial");
}

// The About fields list IDs and names, the version and the storage use;
// they are rewritten only when something changed.
(:test)
function settingsWritesTheAboutFields(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var ids = vault.ids();
    var s = SettingsFixtures.store();
    var job = SettingsFixtures.run(vault, s);
    var catalog = SettingsFixtures.text(s, "catalog");
    var expected = ids[0] + " GitHub (alice@example.com) · " + ids[1] + " VPN (alice@example.com) · " + ids[2] + " Gym";
    if (!catalog.equals(expected)) {
        logger.error("catalog: " + catalog);
        return false;
    }
    var fresh = !AboutScan.needed(vault) && job.forms == 0 && job.lines.size() == 0;
    vault.add(StorageFixtures.totp("New"));
    return fresh && AboutScan.needed(vault) && SettingsFixtures.text(s, "version").equals(AppInfo.VERSION)
        && SettingsFixtures.contains(logger, s, "storageUse", "3 of 150 entries, 1 KB of about 100 KB");
}

// A long catalog is cut with a count of what's left out.
(:test)
function settingsShortensTheCatalog(logger as Test.Logger) as Boolean {
    var vault = SettingsFixtures.vault();
    for (var i = 0; i < 60; i++) {
        var e = StorageFixtures.totp("Service number " + i);
        vault.add(e);
    }
    var s = SettingsFixtures.store();
    SettingsFixtures.run(vault, s);
    var catalog = SettingsFixtures.text(s, "catalog");
    return catalog.length() <= AboutScan.CATALOG_MAX_CHARS + 60 && catalog.find("more (see Entry IDs on the watch)") != null;
}

// "Encrypt names" moves names into the encrypted part of every entry it
// can reach, keeps them off the phone's catalog, and is undone when off.
// Locked entries wait for the session.
(:test)
function settingsAppliesEncryptNames(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var bank = ManageFixtures.addLocked(vault, "Bank");
    vault.lock();
    vault.encryptNames = true;
    var s = SettingsFixtures.store();
    SettingsFixtures.run(vault, s);
    var ids = vault.ids();
    var unlockedDone = (vault.anyHeader(ids[0]) as Entry).namesEncrypted && (vault.anyHeader(ids[0]) as Entry).name.equals("")
        && (vault.header(ids[0]) as Entry).name.equals("GitHub");
    var lockedWaits = !(vault.anyHeader(bank) as Entry).namesEncrypted && AboutScan.needed(vault);
    var catalog = SettingsFixtures.text(s, "catalog");
    var namesHidden = catalog.find("GitHub") == null && catalog.find(ids[0] + " TOTP") != null && catalog.find(ids[2] + " EAN-13") != null;
    StorageFixtures.unlock(vault, StorageFixtures.PIN);
    SettingsFixtures.run(vault, s);
    var lockedDone = (vault.anyHeader(bank) as Entry).namesEncrypted && !AboutScan.needed(vault);
    vault.encryptNames = false;
    SettingsFixtures.run(vault, s);
    var back = !(vault.anyHeader(ids[0]) as Entry).namesEncrypted && (vault.anyHeader(ids[0]) as Entry).name.equals("GitHub")
        && SettingsFixtures.text(s, "catalog").find("GitHub") != null;
    // A new entry follows the option.
    vault.encryptNames = true;
    var id = vault.add(StorageFixtures.totp("Later"));
    var follows = (vault.anyHeader(id) as Entry).namesEncrypted;
    if (!(unlockedDone && lockedWaits && namesHidden && lockedDone && back && follows)) {
        logger.error([unlockedDone, lockedWaits, namesHidden, lockedDone, back, follows].toString() + " " + catalog);
        return false;
    }
    return true;
}

// hasWork: nothing to do once the About fields are current and every
// switch is off.
(:test)
function settingsHasWorkOnlyWhenNeeded(logger as Test.Logger) as Boolean {
    var vault = SettingsFixtures.vault();
    var s = SettingsFixtures.store();
    var before = SettingsJob.hasWork(vault, s);
    SettingsFixtures.run(vault, s);
    var after = SettingsJob.hasWork(vault, s);
    s.set(CodeForm.SWITCH, true);
    return before && !after && SettingsJob.hasWork(vault, s);
}

// The watch's side of the properties contract (tools/check_resources.py
// checks the phone's side): every field the forms read exists in
// properties.xml (reading an undefined property crashes the app) and the
// values a cleared form goes back to are the properties' defaults.
(:test)
function settingsDefaultsMatchProperties(logger as Test.Logger) as Boolean {
    var forms = [OtpForm.defaults(), CodeForm.defaults(), ManageForm.defaults()] as Array<Dictionary>;
    var ok = true;
    for (var f = 0; f < forms.size(); f++) {
        var d = forms[f];
        var keys = d.keys();
        for (var i = 0; i < keys.size(); i++) {
            var key = keys[i] as String;
            var actual = Application.Properties.getValue(key);
            var expected = (d[key] as Object).toString();
            if (actual == null || !actual.toString().equals(expected)) {
                logger.error(key + ": property " + actual + ", form default " + expected);
                ok = false;
            }
        }
    }
    var others = [OtpForm.SWITCH, OtpForm.STATUS, CodeForm.SWITCH, CodeForm.STATUS, ManageForm.SWITCH, ManageForm.STATUS,
        "impApply", "impStatus", "impMode", "impLocked", "catalog", "version", "storageUse"];
    for (var i = 0; i < others.size(); i++) {
        if (Application.Properties.getValue(others[i] as String) == null) {
            logger.error((others[i] as String) + " is missing");
            ok = false;
        }
    }
    for (var i = 1; i <= SettingsJob.IMPORT_TEXT_FIELDS; i++) {
        ok = ok && Application.Properties.getValue("impText" + i) != null;
    }
    return ok;
}