Commit 4e67adb664

4e67adb664b1f6c7461c21b333b56ab5f3232f22

parent: 3de1ec4d23

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-19 16:16 UTC

Run the live smoke suite as the ios-smoke account

The suite no longer depends on a personal account: the scratch repo,
the edit fixture, the org member row, the PGP UID and the profile
description all come from a dedicated ios-smoke user (a krz admin).
gitbayUITests/smoke-account.sh provisions it and seeds
ios-smoke/ui-smoke-edit; cmc/ui-smoke-edit is gone. The iPad
screenshot checkpoint now waits for the profile edit button instead of
a specific description, so store screenshots can still be taken as a
real user.

Layout: unified · split

README.org +15
@@ -79,6 +79,21 @@ xcodebuild -project gitbay.xcodeproj -scheme gitbay \
79 -destination 'platform=iOS Simulator,name=iPhone 17' test 79 -destination 'platform=iOS Simulator,name=iPhone 17' test
80#+end_src 80#+end_src
81 81
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 -i ~/.ssh/gitbay-ios-smoke -o IdentitiesOnly=yes -o ControlPath=none \
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
82* Contributing 97* Contributing
83 98
84To contribute, create a merge request with your changes: 99To contribute, create a merge request with your changes:
gitbayUITests/LiveSmokeUITests.swift +32 −20
@@ -10,8 +10,10 @@ import XCTest
10/// TEST_RUNNER_GITBAY_UITEST_LIVE=1 xcodebuild test \ 10/// TEST_RUNNER_GITBAY_UITEST_LIVE=1 xcodebuild test \
11/// -only-testing:gitbayUITests ... 11/// -only-testing:gitbayUITests ...
12/// 12///
13/// They assume the simulator is already signed in as an account that can 13/// They run as the `ios-smoke` account, a krz admin that owns the
14/// write to krz/gitbay-ios, and that branch `ui-smoke` exists. 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.
15final class LiveSmokeUITests: XCTestCase { 17final class LiveSmokeUITests: XCTestCase {
16 18
17 private var app: XCUIApplication! 19 private var app: XCUIApplication!
@@ -22,7 +24,15 @@ final class LiveSmokeUITests: XCTestCase {
22 /// because the name is taken, which reads like a UI bug and is not. 24 /// because the name is taken, which reads like a UI bug and is not.
23 /// Removed before and after, so neither a crashed run nor this one 25 /// Removed before and after, so neither a crashed run nor this one
24 /// can strand it. 26 /// 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"
26 36
27 override func setUpWithError() throws { 37 override func setUpWithError() throws {
28 try XCTSkipUnless( 38 try XCTSkipUnless(
@@ -408,15 +418,15 @@ final class LiveSmokeUITests: XCTestCase {
408 /// The edit flow saves a change to a real file. Seeding a 418 /// The edit flow saves a change to a real file. Seeding a
409 /// repository's first commit is a push, which no test can make, so 419 /// repository's first commit is a push, which no test can make, so
410 /// this only checks — and says why, rather than failing later at 420 /// 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".
412 private func requireFixtureRepo() throws { 422 private func requireFixtureRepo() throws {
413 guard let data = readCommand(["repo", "show", "cmc/ui-smoke-edit"]), 423 guard let data = readCommand(["repo", "show", Self.editRepo]),
414 let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any] 424 let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
415 else { return } // no token, or the instance is unreachable 425 else { return } // no token, or the instance is unreachable
416 try XCTSkipUnless(envelope["data"] != nil, """ 426 try XCTSkipUnless(envelope["data"] != nil, """
417 cmc/ui-smoke-edit is missing. The edit flow saves a change to \ 427 \(Self.editRepo) is missing. The edit flow saves a change to \
418 notes.txt in it; recreate the repo with that file (see its \ 428 notes.txt in it; gitbayUITests/smoke-account.sh seeds it (or \
419 README) and run again. 429 push it by hand, see that script) and run again.
420 """) 430 """)
421 } 431 }
422 432
@@ -533,11 +543,11 @@ extension LiveSmokeUITests {
533 let createSearch = app.searchFields.firstMatch 543 let createSearch = app.searchFields.firstMatch
534 XCTAssertTrue(createSearch.waitForExistence(timeout: 10)) 544 XCTAssertTrue(createSearch.waitForExistence(timeout: 10))
535 focusAndType(createSearch, "ui-smoke") 545 focusAndType(createSearch, "ui-smoke")
536 XCTAssertTrue(app.staticTexts["cmc/ui-smoke"].firstMatch 546 XCTAssertTrue(app.staticTexts[Self.scratchRepo].firstMatch
537 .waitForExistence(timeout: 15), "created repo not in the list") 547 .waitForExistence(timeout: 15), "created repo not in the list")
538 548
539 // --- access: grant a user on the scratch repo, then revoke --- 549 // --- 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()
541 XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10)) 551 XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10))
542 app.staticTexts["Settings"].firstMatch.tap() 552 app.staticTexts["Settings"].firstMatch.tap()
543 let grantUser = app.descendants(matching: .any) 553 let grantUser = app.descendants(matching: .any)
@@ -582,7 +592,7 @@ extension LiveSmokeUITests {
582 592
583 // --- rename the scratch repo there and back --- 593 // --- rename the scratch repo there and back ---
584 // Each rename pushes the renamed repository; the round-trip ends 594 // 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.
586 var firstRename = true 596 var firstRename = true
587 let rename: (String, String) -> Void = { newName, expectedPath in 597 let rename: (String, String) -> Void = { newName, expectedPath in
588 if firstRename { 598 if firstRename {
@@ -616,8 +626,8 @@ extension LiveSmokeUITests {
616 XCTAssertTrue(self.app.staticTexts[expectedPath].firstMatch.waitForExistence(timeout: 15), 626 XCTAssertTrue(self.app.staticTexts[expectedPath].firstMatch.waitForExistence(timeout: 15),
617 "renamed repo did not open as \(expectedPath)") 627 "renamed repo did not open as \(expectedPath)")
618 } 628 }
619 rename("ui-smoke-2", "cmc/ui-smoke-2") 629 rename("ui-smoke-2", Self.scratchRepo + "-2")
620 rename("ui-smoke", "cmc/ui-smoke") 630 rename("ui-smoke", Self.scratchRepo)
621 631
622 // --- pin / unpin round-trip on krz/gitbay-ios --- 632 // --- pin / unpin round-trip on krz/gitbay-ios ---
623 // Start the list fresh; the rename left the stack several deep. 633 // Start the list fresh; the rename left the stack several deep.
@@ -939,7 +949,7 @@ extension LiveSmokeUITests {
939 949
940 // The PGP key's UID email, below the keys on a List that does not 950 // The PGP key's UID email, below the keys on a List that does not
941 // build rows it has not shown. 951 // build rows it has not shown.
942 XCTAssertTrue(scrollTo(app.staticTexts["hello@cleberg.net"].firstMatch), 952 XCTAssertTrue(scrollTo(app.staticTexts[Self.smokePGPEmail].firstMatch),
943 "PGP key UID missing") 953 "PGP key UID missing")
944 954
945 // Activity mail flips off and back on; the value comes back from 955 // Activity mail flips off and back on; the value comes back from
@@ -1010,7 +1020,7 @@ extension LiveSmokeUITests {
1010 orgRow.tap() 1020 orgRow.tap()
1011 1021
1012 // Members render with roles. 1022 // Members render with roles.
1013 XCTAssertTrue(app.staticTexts["cmc"].firstMatch 1023 XCTAssertTrue(app.staticTexts[Self.smokeUser].firstMatch
1014 .waitForExistence(timeout: 15), "org members missing") 1024 .waitForExistence(timeout: 15), "org members missing")
1015 1025
1016 // Create a team. 1026 // Create a team.
@@ -1176,7 +1186,7 @@ extension LiveSmokeUITests {
1176 // previous query, and clearing it from the harness is unreliable. 1186 // previous query, and clearing it from the harness is unreliable.
1177 app.terminate() 1187 app.terminate()
1178 app.launch() 1188 app.launch()
1179 openRepo("cmc/ui-smoke-edit") 1189 openRepo(Self.editRepo)
1180 app.staticTexts["Files"].firstMatch.tap() 1190 app.staticTexts["Files"].firstMatch.tap()
1181 let notes = app.staticTexts["notes.txt"].firstMatch 1191 let notes = app.staticTexts["notes.txt"].firstMatch
1182 XCTAssertTrue(notes.waitForExistence(timeout: 15), "notes.txt not in the tree") 1192 XCTAssertTrue(notes.waitForExistence(timeout: 15), "notes.txt not in the tree")
@@ -1242,7 +1252,7 @@ extension LiveSmokeUITests {
1242 1252
1243 // --- a profile is a profile: description, links, orgs, graph, repos --- 1253 // --- a profile is a profile: description, links, orgs, graph, repos ---
1244 XCTAssertTrue(app.staticTexts 1254 XCTAssertTrue(app.staticTexts
1245 .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch 1255 .containing(NSPredicate(format: "label CONTAINS %@", Self.smokeProfileDescription)).firstMatch
1246 .waitForExistence(timeout: 20), "profile description missing") 1256 .waitForExistence(timeout: 20), "profile description missing")
1247 XCTAssertTrue(header("Organizations").exists, 1257 XCTAssertTrue(header("Organizations").exists,
1248 "org memberships missing") 1258 "org memberships missing")
@@ -1467,8 +1477,10 @@ extension LiveSmokeUITests {
1467 snap("feed") 1477 snap("feed")
1468 1478
1469 selectTab("My Profile") 1479 selectTab("My Profile")
1470 XCTAssertTrue(app.staticTexts 1480 // Any signed-in account can edit its own profile, so this holds
1471 .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch 1481 // for store screenshots taken as a real user too.
1482 XCTAssertTrue(app.descendants(matching: .any)
1483 .matching(identifier: "profile-edit-button").firstMatch
1472 .waitForExistence(timeout: 20), "profile did not load") 1484 .waitForExistence(timeout: 20), "profile did not load")
1473 snap("profile") 1485 snap("profile")
1474 } 1486 }
@@ -1631,7 +1643,7 @@ extension LiveSmokeUITests {
1631 let orgRow = app.staticTexts[Self.smokeOrg].firstMatch 1643 let orgRow = app.staticTexts[Self.smokeOrg].firstMatch
1632 XCTAssertTrue(orgRow.waitForExistence(timeout: 15), "org list missing") 1644 XCTAssertTrue(orgRow.waitForExistence(timeout: 15), "org list missing")
1633 orgRow.tap() 1645 orgRow.tap()
1634 XCTAssertTrue(app.staticTexts["cmc"].firstMatch.waitForExistence(timeout: 15), 1646 XCTAssertTrue(app.staticTexts[Self.smokeUser].firstMatch.waitForExistence(timeout: 15),
1635 "org members missing") 1647 "org members missing")
1636 1648
1637 // --- labels --- 1649 // --- labels ---
gitbayUITests/smoke-account.sh added +65
@@ -0,0 +1,65 @@
1#!/bin/sh
2# Provision the ios-smoke account the live UI smoke suite runs as. Run
3# once, as an instance admin, where the gitbay CLI is signed in. It
4# creates the account with a fresh SSH key (~/.ssh/gitbay-ios-smoke),
5# makes it a krz admin, sets its profile description, registers a PGP
6# key, and pushes the ios-smoke/ui-smoke-edit fixture that
7# testBlameAndEditFlows edits. LiveSmokeUITests asserts on the same
8# 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
18gitbay admin user create "$user" --email "$user@gitbay.org" --verified --key - < "$key.pub"
19gitbay org members add krz "$user" --role admin
20
21# Everything below runs as the account. No multiplexing: the CLI's
22# control socket would otherwise answer as the admin.
23as_smoke() {
24 ssh -i "$key" -o IdentitiesOnly=yes -o ControlPath=none git@gitbay.org "$@"
25}
26
27as_smoke profile set --description "'$description'"
28
29# The PGP key is a display fixture; its private half is discarded.
30gnupg=$(mktemp -d /tmp/pgp.XXXXXX)
31GNUPGHOME=$gnupg gpg --batch --quiet --quick-generate-key "$user <$pgp_email>" ed25519 sign never
32GNUPGHOME=$gnupg gpg --armor --export "$pgp_email" | as_smoke pgp add
33GNUPGHOME=$gnupg gpgconf --kill all
34rm -rf "$gnupg"
35
36as_smoke repo create "$user/ui-smoke-edit"
37work=$(mktemp -d)
38git -C "$work" init -q -b main
39cat > "$work/README.md" <<'EOF'
40# ui-smoke-edit
41
42A fixture repository for the gitbay-ios live UI smoke suite. `notes.txt`
43is edited by `testBlameAndEditFlows` on every run.
44
45Nothing in the app or the JSON API can seed a repository's first commit
46(that is a push), so the suite only checks that this repo exists and
47skips when it does not. `gitbayUITests/smoke-account.sh` in krz/gitbay-ios
48is what created it.
49EOF
50cat > "$work/notes.txt" <<'EOF'
51Scratch content for the live UI smoke suite.
52
53testBlameAndEditFlows opens this file and saves an edit through the app,
54which is how the file-editing path is verified end to end. The text does
55not matter; the file existing does.
56EOF
57git -C "$work" add -A
58git -C "$work" -c user.name="$user" -c user.email="$user@gitbay.org" -c commit.gpgsign=false \
59 commit -q -m 'fixture repo for the live smoke suite'
60GIT_SSH_COMMAND="ssh -i $key -o IdentitiesOnly=yes -o ControlPath=none" \
61 git -C "$work" push -q "ssh://git@gitbay.org/$user/ui-smoke-edit.git" main
62rm -rf "$work"
63
64echo "Mint the suite's token with:"
65echo " ssh -i $key -o IdentitiesOnly=yes -o ControlPath=none git@gitbay.org token create --name ui-smoke --scope full --ttl 30d"