• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 35267216991

17 Sep 2026 07:50PM UTC coverage: 77.023% (+0.2%) from 76.864%
35267216991

push

github

web-flow
A first-run wizard instead of message boxes before the Settings dialog (#1816)

* A first-run wizard instead of message boxes before the Settings dialog

ConfigDialog::wizard() was a chain of modal boxes that ran before the
whole configuration dialog was shown: GnuPG missing, generate a key,
create the store?, pick recipients, then the full Settings dialog with
its four tabs for someone who had not used the program yet. Declining
"Create password-store?" left an invalid configuration and the dialog
came back. It mutated ConfigDialog widgets, drove KeygenDialog and
UsersDialog through the shared backend and re-pointed the backend at the
new store from inside the dialog.

FirstRunWizard is a QWizard with four pages after the intro: Programs
(gpg required, runnable check on the path, pass and Git optional and only
offerable when found), Your key (the secret keys the chosen gpg lists,
tickable, with a Generate button that runs KeygenDialog), Password store
(an existing store is used as it is, an empty or missing folder is
described and set up on Finish) and Ready (summary, hide passwords, tray
icon). It edits a copy of the settings; accept() creates and initialises
the store through ProfileInit for the ticked keys, saves the settings
and invalidates the backend. MainWindow::config() runs it on a fresh
start and the ConfigDialog otherwise; the constructor's loop is unchanged.
The six wizard helpers leave ConfigDialog.

tst_firstrunwizard drives the pages with a scripted gpg (page gating,
key listing and ticking, store status, Finish creating .gpg-id with the
fingerprint, an existing store left alone). tst_mainwindow's first-run
tests now drive the wizard: cancel stops startup, Finish sets up the
store and startup succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* First-run wizard: Git decided per folder, unusable keys unticked, no early writes

Review of the wizard tur... (continued)

291 of 361 new or added lines in 4 files covered. (80.61%)

22 existing lines in 3 files now uncovered.

6349 of 8243 relevant lines covered (77.02%)

72.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.91
/src/profileinit.cpp
1
// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "profileinit.h"
4
#include "appsettings.h"
5
#include "executor.h"
6
#include "gpgidsigner.h"
7
#include "userinfo.h"
8
#include <QDir>
9
#include <QDirIterator>
10
#include <QFile>
11
#include <QFileInfo>
12
#include <QProcess>
13

14
auto ProfileInit::needsInit(const QString &path) -> bool {
7 ✔
15
  if (path.isEmpty()) {
7 ✔
16
    return false;
17
  }
18
  QDir dir(path);
6 ✔
19
  if (!dir.exists()) {
6 ✔
20
    return false;
21
  }
22
  return !dir.exists(".gpg-id");
5 ✔
23
}
6 ✔
24

25
auto ProfileInit::initialise(const QString &dir, const QList<UserInfo> &users,
7 ✔
26
                             const AppSettings &s, bool useGit, QString *note)
27
    -> bool {
28
  QString scratch;
7 ✔
29
  QString &out = note != nullptr ? *note : scratch;
7 ✔
30
  out.clear();
7 ✔
31

32
  const QDir folder(dir);
7 ✔
33
  if (!folder.exists() && !QDir().mkpath(folder.absolutePath())) {
10 ✔
34
    out = tr("Could not create %1.").arg(folder.absolutePath());
×
35
    return false;
×
36
  }
37
  const QString gpgIdFile = folder.filePath(QStringLiteral(".gpg-id"));
14 ✔
38
  if (!writeGpgId(gpgIdFile, users, &out)) {
7 ✔
39
    return false;
40
  }
41
  QString sigFile;
6 ✔
42
  if (!s.passSigningKey.trimmed().isEmpty()) {
12 ✔
43
    if (!signGpgId(gpgIdFile, s, &out)) {
×
44
      return false;
45
    }
46
    sigFile = gpgIdFile + QStringLiteral(".sig");
×
47
  }
48
  if (useGit &&
7 ✔
49
      !commitGpgId(folder.absolutePath(), gpgIdFile, sigFile, s, &out)) {
7 ✔
50
    return false;
51
  }
52

53
  const QStringList existing = folder.entryList(
6 ✔
54
      {QStringLiteral("*.gpg")}, QDir::Files | QDir::NoDotAndDotDot);
18 ✔
55
  if (!existing.isEmpty()) {
6 ✔
56
    out = tr("%1 already contains %n encrypted file(s); they were not "
1 ✔
57
             "re-encrypted. Switch to the profile and open Users to do that.",
58
             nullptr, static_cast<int>(existing.size()))
59
              .arg(folder.absolutePath());
2 ✔
60
  }
61
  return true;
62
}
7 ✔
63

64
auto ProfileInit::writeGpgId(const QString &gpgIdFile,
7 ✔
65
                             const QList<UserInfo> &users, QString *note)
66
    -> bool {
67
  QStringList ids;
7 ✔
68
  for (const UserInfo &user : users) {
19 ✔
69
    if (user.enabled) {
12 ✔
70
      ids << user.key_id;
6 ✔
71
    }
72
  }
73
  if (ids.isEmpty()) {
7 ✔
74
    *note = tr("No recipient selected; %1 was not written.").arg(gpgIdFile);
2 ✔
75
    return false;
1 ✔
76
  }
77
  QFile file(gpgIdFile);
6 ✔
78
  if (!file.open(QIODevice::WriteOnly | QIODevice::Text) ||
12 ✔
79
      file.write((ids.join(QLatin1Char('\n')) + QLatin1Char('\n')).toUtf8()) <
36 ✔
80
          0) {
81
    *note = tr("Could not write %1: %2").arg(gpgIdFile, file.errorString());
×
82
    return false;
×
83
  }
84
  file.close();
6 ✔
85
  // Same as ImitatePass::writeGpgIdFile: the recipient list leaks which keys
86
  // the store is encrypted to, so keep it owner-only where that means
87
  // anything.
88
  QFile::setPermissions(gpgIdFile, QFile::ReadOwner | QFile::WriteOwner);
6 ✔
89
  return true;
90
}
6 ✔
91

92
auto ProfileInit::signGpgId(const QString &gpgIdFile, const AppSettings &s,
×
93
                            QString *note) -> bool {
94
  const GpgIdSigner signer(s.gpgExecutable,
95
                           GpgIdSigner::keysFromSetting(s.passSigningKey));
×
96
  QString err;
×
97
  if (!signer.sign(gpgIdFile, &err)) {
×
98
    *note = tr("Could not sign %1 with %2: %3")
×
99
                .arg(gpgIdFile, signer.keys().first(), err.trimmed());
×
100
    return false;
×
101
  }
102
  return true;
103
}
×
104

105
auto ProfileInit::commitGpgId(const QString &dir, const QString &gpgIdFile,
1 ✔
106
                              const QString &sigFile, const AppSettings &s,
107
                              QString *note) -> bool {
108
  // A process of our own in the profile directory: nothing here may touch
109
  // the shared Executor or PASSWORD_STORE_DIR of the active store.
110
  QProcess git;
1 ✔
111
  git.setWorkingDirectory(dir);
1 ✔
112
  auto run = [&](const QStringList &args) -> bool {
3 ✔
113
    QString err;
3 ✔
114
    const int rc = Executor::executeBlocking(git, s.gitExecutable, args,
3 ✔
115
                                             QString(), nullptr, &err);
3 ✔
116
    if (rc != 0) {
3 ✔
117
      *note =
118
          tr("git %1 failed in %2: %3").arg(args.first(), dir, err.trimmed());
×
119
      return false;
×
120
    }
121
    return true;
122
  };
1 ✔
123
  QStringList files{QFileInfo(gpgIdFile).fileName()};
3 ✔
124
  if (!sigFile.isEmpty()) {
1 ✔
125
    files << QFileInfo(sigFile).fileName();
×
126
  }
127
  const QStringList add =
128
      QStringList{QStringLiteral("add"), QStringLiteral("--")} + files;
4 ✔
129
  const QStringList commit =
130
      QStringList{QStringLiteral("commit"), QStringLiteral("-m"),
7 ✔
131
                  QStringLiteral("Added .gpg-id using QtPass."),
1 ✔
132
                  QStringLiteral("--")} +
5 ✔
133
      files;
134
  return run({QStringLiteral("init")}) && run(add) && run(commit);
4 ✔
135
}
2 ✔
136

NEW
137
auto ProfileInit::initGit(const QString &dir, const AppSettings &s,
×
138
                          QString *note) -> bool {
NEW
139
  QProcess git;
×
NEW
140
  git.setWorkingDirectory(dir);
×
NEW
141
  auto run = [&](const QStringList &args) -> bool {
×
NEW
142
    QString err;
×
NEW
143
    const int rc = Executor::executeBlocking(git, s.gitExecutable, args,
×
NEW
144
                                             QString(), nullptr, &err);
×
NEW
145
    if (rc != 0) {
×
146
      *note =
NEW
147
          tr("git %1 failed in %2: %3").arg(args.first(), dir, err.trimmed());
×
NEW
148
      return false;
×
149
    }
150
    return true;
NEW
151
  };
×
152
  // Only what belongs to a store is staged: an existing folder may hold
153
  // exports, editor swap files or other plaintext that must not enter the
154
  // history (the same rule the re-encryption backup commit follows).
NEW
155
  QStringList files;
×
156
  QDirIterator it(dir,
NEW
157
                  {QStringLiteral("*.gpg"), QStringLiteral(".gpg-id"),
×
NEW
158
                   QStringLiteral(".gpg-id.sig")},
×
NEW
159
                  QDir::Files | QDir::Hidden, QDirIterator::Subdirectories);
×
NEW
160
  const QDir base(dir);
×
NEW
161
  while (it.hasNext()) {
×
NEW
162
    const QString rel = base.relativeFilePath(it.next());
×
NEW
163
    if (!rel.startsWith(QStringLiteral(".git/"))) {
×
164
      files << rel;
165
    }
166
  }
NEW
167
  if (!run({QStringLiteral("init")})) {
×
168
    return false;
169
  }
NEW
170
  if (!files.isEmpty() &&
×
NEW
171
      !run(QStringList{QStringLiteral("add"), QStringLiteral("--")} + files)) {
×
172
    return false;
173
  }
NEW
174
  return run({QStringLiteral("commit"), QStringLiteral("--allow-empty"),
×
NEW
175
              QStringLiteral("-m"),
×
NEW
176
              QStringLiteral("Added password store using QtPass.")});
×
NEW
177
}
×
178

179
auto ProfileInit::gitIdentityConfigured(const QString &dir,
4 ✔
180
                                        const AppSettings &s) -> bool {
181
  QProcess git;
4 ✔
182
  git.setWorkingDirectory(dir);
4 ✔
183
  for (const char *key : {"user.name", "user.email"}) {
8 ✔
184
    QString out;
6 ✔
185
    if (Executor::executeBlocking(git, s.gitExecutable,
30 ✔
186
                                  {QStringLiteral("config"),
6 ✔
187
                                   QStringLiteral("--get"),
6 ✔
188
                                   QString::fromLatin1(key)},
189
                                  QString(), &out, nullptr) != 0 ||
16 ✔
190
        out.trimmed().isEmpty()) {
6 ✔
191
      return false;
192
    }
193
  }
194
  return true;
2 ✔
195
}
16 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc