Run the live smoke suite as the ios-smoke account !109

merged merged by cmc on 2026-09-19 19:29 UTC · krz/gitbay-ios:smoke-account into main

3 files changed, +122 −20

Layout: unified · split

README.org +15
@@ -79,6 +79,21 @@ xcodebuild -project gitbay.xcodeproj -scheme gitbay \
7979 -destination 'platform=iOS Simulator,name=iPhone 17' test
8080#+end_src
8181
82The suite runs as the =ios-smoke= account, not a personal one: it owns the
83scratch and fixture repositories and is an admin of =krz=. An instance admin
84provisions it once with =gitbayUITests/smoke-account.sh=, which also writes the
85account's SSH key to =~/.ssh/gitbay-ios-smoke=. The token above is minted with
86that key:
87
88#+begin_src sh
89ssh -F /dev/null -i ~/.ssh/gitbay-ios-smoke -o IdentitiesOnly=yes \
90 git@gitbay.org token create --name ui-smoke --scope full --ttl 30d
91#+end_src
92
93Setup signs in only when it finds the token screen, so a simulator signed in as
94another account has to be signed out first (=xcrun simctl keychain <device>
95reset= while the app is not running).
96
8297* Contributing
8398
8499To contribute, create a merge request with your changes:
gitbayUITests/LiveSmokeUITests.swift +32 −20
@@ -10,8 +10,10 @@ import XCTest
1010/// TEST_RUNNER_GITBAY_UITEST_LIVE=1 xcodebuild test \
1111/// -only-testing:gitbayUITests ...
1212///
13/// They assume the simulator is already signed in as an account that can
14/// write to krz/gitbay-ios, and that branch `ui-smoke` exists.
13/// They run as the `ios-smoke` account, a krz admin that owns the
14/// scratch and fixture repositories (smoke-account.sh provisions it), and
15/// assume branch `ui-smoke` exists on krz/gitbay-ios. A signed-out device
16/// is signed in at setUp from TEST_RUNNER_GITBAY_UITEST_TOKEN.
1517final class LiveSmokeUITests: XCTestCase {
1618
1719 private var app: XCUIApplication!
@@ -22,7 +24,15 @@ final class LiveSmokeUITests: XCTestCase {
2224 /// because the name is taken, which reads like a UI bug and is not.
2325 /// Removed before and after, so neither a crashed run nor this one
2426 /// can strand it.
25 private static let scratchRepo = "cmc/ui-smoke"
27 private static let scratchRepo = "\(smokeUser)/ui-smoke"
28
29 /// The account the suite runs as. smoke-account.sh creates it with
30 /// the profile description, PGP key and fixture repository the
31 /// assertions below read back.
32 static let smokeUser = "ios-smoke"
33 static let smokeProfileDescription = "Live UI smoke account for gitbay-ios"
34 static let smokePGPEmail = "ios-smoke+pgp@gitbay.org"
35 static let editRepo = "\(smokeUser)/ui-smoke-edit"
2636
2737 override func setUpWithError() throws {
2838 try XCTSkipUnless(
@@ -408,15 +418,15 @@ final class LiveSmokeUITests: XCTestCase {
408418 /// The edit flow saves a change to a real file. Seeding a
409419 /// repository's first commit is a push, which no test can make, so
410420 /// this only checks — and says why, rather than failing later at
411 /// "cmc/ui-smoke-edit not in the repo list".
421 /// "ios-smoke/ui-smoke-edit not in the repo list".
412422 private func requireFixtureRepo() throws {
413 guard let data = readCommand(["repo", "show", "cmc/ui-smoke-edit"]),
423 guard let data = readCommand(["repo", "show", Self.editRepo]),
414424 let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
415425 else { return } // no token, or the instance is unreachable
416426 try XCTSkipUnless(envelope["data"] != nil, """
417 cmc/ui-smoke-edit is missing. The edit flow saves a change to \
418 notes.txt in it; recreate the repo with that file (see its \
419 README) and run again.
427 \(Self.editRepo) is missing. The edit flow saves a change to \
428 notes.txt in it; gitbayUITests/smoke-account.sh seeds it (or \
429 push it by hand, see that script) and run again.
420430 """)
421431 }
422432
@@ -533,11 +543,11 @@ extension LiveSmokeUITests {
533543 let createSearch = app.searchFields.firstMatch
534544 XCTAssertTrue(createSearch.waitForExistence(timeout: 10))
535545 focusAndType(createSearch, "ui-smoke")
536 XCTAssertTrue(app.staticTexts["cmc/ui-smoke"].firstMatch
546 XCTAssertTrue(app.staticTexts[Self.scratchRepo].firstMatch
537547 .waitForExistence(timeout: 15), "created repo not in the list")
538548
539549 // --- access: grant a user on the scratch repo, then revoke ---
540 app.staticTexts["cmc/ui-smoke"].firstMatch.tap()
550 app.staticTexts[Self.scratchRepo].firstMatch.tap()
541551 XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10))
542552 app.staticTexts["Settings"].firstMatch.tap()
543553 let grantUser = app.descendants(matching: .any)
@@ -582,7 +592,7 @@ extension LiveSmokeUITests {
582592
583593 // --- rename the scratch repo there and back ---
584594 // Each rename pushes the renamed repository; the round-trip ends
585 // on cmc/ui-smoke again, several screens deep.
595 // on the scratch repo again, several screens deep.
586596 var firstRename = true
587597 let rename: (String, String) -> Void = { newName, expectedPath in
588598 if firstRename {
@@ -616,8 +626,8 @@ extension LiveSmokeUITests {
616626 XCTAssertTrue(self.app.staticTexts[expectedPath].firstMatch.waitForExistence(timeout: 15),
617627 "renamed repo did not open as \(expectedPath)")
618628 }
619 rename("ui-smoke-2", "cmc/ui-smoke-2")
620 rename("ui-smoke", "cmc/ui-smoke")
629 rename("ui-smoke-2", Self.scratchRepo + "-2")
630 rename("ui-smoke", Self.scratchRepo)
621631
622632 // --- pin / unpin round-trip on krz/gitbay-ios ---
623633 // Start the list fresh; the rename left the stack several deep.
@@ -939,7 +949,7 @@ extension LiveSmokeUITests {
939949
940950 // The PGP key's UID email, below the keys on a List that does not
941951 // build rows it has not shown.
942 XCTAssertTrue(scrollTo(app.staticTexts["hello@cleberg.net"].firstMatch),
952 XCTAssertTrue(scrollTo(app.staticTexts[Self.smokePGPEmail].firstMatch),
943953 "PGP key UID missing")
944954
945955 // Activity mail flips off and back on; the value comes back from
@@ -1010,7 +1020,7 @@ extension LiveSmokeUITests {
10101020 orgRow.tap()
10111021
10121022 // Members render with roles.
1013 XCTAssertTrue(app.staticTexts["cmc"].firstMatch
1023 XCTAssertTrue(app.staticTexts[Self.smokeUser].firstMatch
10141024 .waitForExistence(timeout: 15), "org members missing")
10151025
10161026 // Create a team.
@@ -1176,7 +1186,7 @@ extension LiveSmokeUITests {
11761186 // previous query, and clearing it from the harness is unreliable.
11771187 app.terminate()
11781188 app.launch()
1179 openRepo("cmc/ui-smoke-edit")
1189 openRepo(Self.editRepo)
11801190 app.staticTexts["Files"].firstMatch.tap()
11811191 let notes = app.staticTexts["notes.txt"].firstMatch
11821192 XCTAssertTrue(notes.waitForExistence(timeout: 15), "notes.txt not in the tree")
@@ -1242,7 +1252,7 @@ extension LiveSmokeUITests {
12421252
12431253 // --- a profile is a profile: description, links, orgs, graph, repos ---
12441254 XCTAssertTrue(app.staticTexts
1245 .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch
1255 .containing(NSPredicate(format: "label CONTAINS %@", Self.smokeProfileDescription)).firstMatch
12461256 .waitForExistence(timeout: 20), "profile description missing")
12471257 XCTAssertTrue(header("Organizations").exists,
12481258 "org memberships missing")
@@ -1467,8 +1477,10 @@ extension LiveSmokeUITests {
14671477 snap("feed")
14681478
14691479 selectTab("My Profile")
1470 XCTAssertTrue(app.staticTexts
1471 .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch
1480 // Any signed-in account can edit its own profile, so this holds
1481 // for store screenshots taken as a real user too.
1482 XCTAssertTrue(app.descendants(matching: .any)
1483 .matching(identifier: "profile-edit-button").firstMatch
14721484 .waitForExistence(timeout: 20), "profile did not load")
14731485 snap("profile")
14741486 }
@@ -1631,7 +1643,7 @@ extension LiveSmokeUITests {
16311643 let orgRow = app.staticTexts[Self.smokeOrg].firstMatch
16321644 XCTAssertTrue(orgRow.waitForExistence(timeout: 15), "org list missing")
16331645 orgRow.tap()
1634 XCTAssertTrue(app.staticTexts["cmc"].firstMatch.waitForExistence(timeout: 15),
1646 XCTAssertTrue(app.staticTexts[Self.smokeUser].firstMatch.waitForExistence(timeout: 15),
16351647 "org members missing")
16361648
16371649 // --- labels ---
gitbayUITests/smoke-account.sh added +75
@@ -0,0 +1,75 @@
1#!/bin/sh
2# Provision the ios-smoke account the live UI smoke suite runs as. Run as
3# an instance admin where the gitbay CLI is signed in; every step skips
4# what already exists, so a failed run is rerun as is. It creates the
5# account with a fresh SSH key (~/.ssh/gitbay-ios-smoke), makes it a krz
6# admin, sets its profile description, registers a PGP key, and pushes
7# the ios-smoke/ui-smoke-edit fixture that testBlameAndEditFlows edits.
8# LiveSmokeUITests asserts on the same values, so keep the two in step.
9set -eu
10
11user=ios-smoke
12key=$HOME/.ssh/gitbay-$user
13description='Live UI smoke account for gitbay-ios'
14pgp_email="$user+pgp@gitbay.org"
15
16[ -f "$key" ] || ssh-keygen -q -t ed25519 -N '' -C "$user" -f "$key"
17
18if ! gitbay admin user show "$user" --json > /dev/null 2>&1; then
19 gitbay admin user create "$user" --email "$user@gitbay.org" --verified --key - < "$key.pub"
20fi
21gitbay org members add krz "$user" --role admin
22
23# Everything below runs as the account. The user's ssh config is ignored:
24# its identities and control socket would answer as the admin.
25smoke_ssh="ssh -F /dev/null -i $key -o IdentitiesOnly=yes"
26as_smoke() {
27 $smoke_ssh git@gitbay.org "$@"
28}
29
30as_smoke profile set --description "'$description'"
31
32# The PGP key is a display fixture; its private half is discarded.
33if ! as_smoke pgp list --json | grep -q "$pgp_email"; then
34 gnupg=$(mktemp -d /tmp/pgp.XXXXXX)
35 GNUPGHOME=$gnupg gpg --batch --quiet --pinentry-mode loopback --passphrase '' \
36 --quick-generate-key "$user <$pgp_email>" ed25519 sign never
37 GNUPGHOME=$gnupg gpg --armor --export "$pgp_email" | as_smoke pgp add
38 GNUPGHOME=$gnupg gpgconf --kill all
39 rm -rf "$gnupg"
40fi
41
42if as_smoke repo show "$user/ui-smoke-edit" --json > /dev/null 2>&1; then
43 echo "$user/ui-smoke-edit exists"
44 exit 0
45fi
46as_smoke repo create "$user/ui-smoke-edit"
47work=$(mktemp -d)
48git -C "$work" init -q -b main
49cat > "$work/README.md" <<'EOF'
50# ui-smoke-edit
51
52A fixture repository for the gitbay-ios live UI smoke suite. `notes.txt`
53is edited by `testBlameAndEditFlows` on every run.
54
55Nothing in the app or the JSON API can seed a repository's first commit
56(that is a push), so the suite only checks that this repo exists and
57skips when it does not. `gitbayUITests/smoke-account.sh` in krz/gitbay-ios
58is what created it.
59EOF
60cat > "$work/notes.txt" <<'EOF'
61Scratch content for the live UI smoke suite.
62
63testBlameAndEditFlows opens this file and saves an edit through the app,
64which is how the file-editing path is verified end to end. The text does
65not matter; the file existing does.
66EOF
67git -C "$work" add -A
68git -C "$work" -c user.name="$user" -c user.email="$user@gitbay.org" -c commit.gpgsign=false \
69 commit -q -m 'fixture repo for the live smoke suite'
70GIT_SSH_COMMAND="$smoke_ssh" \
71 git -C "$work" push -q "ssh://git@gitbay.org/$user/ui-smoke-edit.git" main
72rm -rf "$work"
73
74echo "Mint the suite's token with:"
75echo " $smoke_ssh git@gitbay.org token create --name ui-smoke --scope full --ttl 30d"