mirror of
https://codeberg.org/secana/Forji.git
synced 2026-06-16 05:13:55 -07:00
The integration seeder gave no visible progress and could hang indefinitely. A `docker compose exec` for the admin-user step wedged on a transient Docker-on-macOS flake, and `DockerExec.run` waited on it with no timeout, blocking the whole UI test suite forever with no output (stdout was block-buffered, so the existing phase prints never flushed). - Line-buffer the seeder's stdout so phase progress streams live instead of being dumped all at once when stdout is a pipe. - Add numbered "[n/7] <phase> (Ns elapsed)" headers for a clear progress and timing signal. - Add a 60s timeout to `docker compose exec` and retry the admin-user step, so a hung exec fails fast and recovers instead of wedging the suite. Reviewed-on: https://codeberg.org/secana/Forji/pulls/69
462 lines
17 KiB
Swift
462 lines
17 KiB
Swift
// swiftlint:disable file_length
|
|
import ForgejoKit
|
|
import Foundation
|
|
|
|
// swiftlint:disable:next type_body_length
|
|
struct Seeder {
|
|
let baseURL: String
|
|
let serviceName: String
|
|
let composeFile: String
|
|
|
|
private let adminUser = "testadmin"
|
|
private let adminPass = "admin1234"
|
|
private let testbotUser = "testbot"
|
|
private let testbotPass = "testbot1234"
|
|
private let readonlyUser = "readonlyuser"
|
|
private let readonlyPass = "readonly1234"
|
|
|
|
func seed() async throws {
|
|
let adminClient = ForgejoClient(
|
|
serverURL: baseURL, username: adminUser, password: adminPass,
|
|
allowSelfSignedCertificates: true,
|
|
)
|
|
let testbotClient = ForgejoClient(
|
|
serverURL: baseURL, username: testbotUser, password: testbotPass,
|
|
allowSelfSignedCertificates: true,
|
|
)
|
|
|
|
let start = Date()
|
|
let total = 7
|
|
func step(_ number: Int, _ label: String) {
|
|
let elapsed = Int(Date().timeIntervalSince(start))
|
|
print("\n[\(number)/\(total)] \(label) (\(elapsed)s elapsed)")
|
|
}
|
|
|
|
step(1, "Creating admin user")
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(2), operation: "create admin user") {
|
|
try createAdminUser()
|
|
}
|
|
step(2, "Creating API users")
|
|
try await createAPIUsers(adminClient: adminClient)
|
|
step(3, "Creating repositories")
|
|
try await createRepositories(adminClient: adminClient)
|
|
step(4, "Creating issues, files, and metadata")
|
|
try await createIssuesFilesMetadata(adminClient: adminClient, testbotClient: testbotClient)
|
|
step(5, "Creating pull requests")
|
|
try await createPullRequests(testbotClient: testbotClient)
|
|
step(6, "Setting pull request metadata")
|
|
try await setPRMetadata(adminClient: adminClient, testbotClient: testbotClient)
|
|
step(7, "Creating API token")
|
|
try await createAPIToken(adminClient: adminClient)
|
|
print("\nSeed data created successfully (\(Int(Date().timeIntervalSince(start)))s total).")
|
|
}
|
|
|
|
// MARK: - Phase 1: Create users
|
|
|
|
private func createAdminUser() throws {
|
|
print("Creating admin user...")
|
|
try DockerExec.run(
|
|
composeFile: composeFile, service: serviceName,
|
|
command: [
|
|
"forgejo", "admin", "user", "create", "--admin",
|
|
"--username", adminUser,
|
|
"--password", adminPass,
|
|
"--email", "testadmin@test.local",
|
|
"--must-change-password=false",
|
|
],
|
|
)
|
|
}
|
|
|
|
private func createAPIUsers(adminClient: ForgejoClient) async throws {
|
|
let adminService = AdminService(client: adminClient)
|
|
|
|
print("Creating testbot user...")
|
|
_ = try await adminService.createUser(
|
|
username: testbotUser, password: testbotPass,
|
|
email: "testbot@test.local",
|
|
)
|
|
|
|
print("Creating readonlyuser...")
|
|
_ = try await adminService.createUser(
|
|
username: readonlyUser, password: readonlyPass,
|
|
email: "readonlyuser@test.local",
|
|
)
|
|
}
|
|
|
|
// MARK: - Phase 2: Create repositories
|
|
|
|
private func createRepositories(adminClient: ForgejoClient) async throws {
|
|
let repoService = RepositoryService(client: adminClient)
|
|
|
|
print("Creating repositories...")
|
|
for (name, desc) in [
|
|
("test-repo", nil as String?),
|
|
("test-repo-2", "A second test repository" as String?),
|
|
("archived-repo", "An archived repository" as String?),
|
|
("html-readme-repo", "Repo with HTML in README" as String?),
|
|
] {
|
|
_ = try await repoService.createRepository(
|
|
name: name, description: desc, autoInit: true,
|
|
)
|
|
}
|
|
print("Repositories created.")
|
|
|
|
print("Archiving archived-repo...")
|
|
try await withRetry(maxAttempts: 5, delay: .seconds(2), operation: "archive repo") {
|
|
_ = try await repoService.editRepository(
|
|
owner: adminUser, repo: "archived-repo", archived: true,
|
|
)
|
|
}
|
|
|
|
// Forgejo 15 enables actions on every new repo by default. Disable it on test-repo-2 so
|
|
// `ActionsUITests.testActionsTabAbsentForRepoWithoutActions` exercises the negative path.
|
|
print("Disabling actions on test-repo-2...")
|
|
try await withRetry(maxAttempts: 5, delay: .seconds(2), operation: "disable actions") {
|
|
_ = try await repoService.editRepository(
|
|
owner: adminUser, repo: "test-repo-2", hasActions: false,
|
|
)
|
|
}
|
|
|
|
print("Updating html-readme-repo README...")
|
|
let readmeSHA: String = try await withRetry(maxAttempts: 5, delay: .seconds(2), operation: "fetch README") {
|
|
let file = try await repoService.fetchFileContent(
|
|
owner: adminUser, repo: "html-readme-repo", path: "README.md",
|
|
)
|
|
return file.sha
|
|
}
|
|
|
|
_ = try await repoService.updateFile(
|
|
owner: adminUser, repo: "html-readme-repo", path: "README.md",
|
|
content: htmlReadmeContent, sha: readmeSHA, message: "Add HTML to README",
|
|
)
|
|
}
|
|
|
|
// MARK: - Phase 3: Issues, files, metadata
|
|
|
|
// swiftlint:disable:next function_body_length
|
|
private func createIssuesFilesMetadata(
|
|
adminClient: ForgejoClient, testbotClient: ForgejoClient,
|
|
) async throws {
|
|
let adminIssueService = IssueService(client: adminClient)
|
|
let testbotIssueService = IssueService(client: testbotClient)
|
|
let repoService = RepositoryService(client: adminClient)
|
|
|
|
print("Creating pagination test issues...")
|
|
async let paginationTask: Void = {
|
|
for index in 1 ... 25 {
|
|
_ = try await adminIssueService.createIssue(
|
|
owner: adminUser, repo: "test-repo-2",
|
|
title: "Pagination test issue \(index)", body: nil,
|
|
)
|
|
}
|
|
}()
|
|
|
|
print("Creating issues as testbot...")
|
|
for index in 1 ... 3 {
|
|
_ = try await testbotIssueService.createIssue(
|
|
owner: adminUser, repo: "test-repo",
|
|
title: "Test issue \(index) from integration tests", body: nil,
|
|
)
|
|
}
|
|
|
|
print("Setting up test-repo content...")
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "comment on issue #1") {
|
|
_ = try await testbotIssueService.createComment(
|
|
owner: adminUser, repo: "test-repo",
|
|
index: 1, body: "This is a test comment from testbot",
|
|
)
|
|
}
|
|
}
|
|
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "edit issue #3") {
|
|
_ = try await adminIssueService.editIssue(
|
|
owner: adminUser, repo: "test-repo", index: 3,
|
|
title: nil,
|
|
body: "This is the **markdown** body for issue 3.\n\nIt has multiple paragraphs.",
|
|
state: "closed",
|
|
)
|
|
}
|
|
}
|
|
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "edit issue #1") {
|
|
_ = try await adminIssueService.editIssue(
|
|
owner: adminUser, repo: "test-repo", index: 1,
|
|
title: nil,
|
|
body: "This is the **markdown** body for issue 1.\n\nIt has multiple paragraphs.",
|
|
state: nil,
|
|
)
|
|
}
|
|
}
|
|
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "edit issue #2") {
|
|
_ = try await adminIssueService.editIssue(
|
|
owner: adminUser, repo: "test-repo", index: 2,
|
|
title: nil,
|
|
body: "This is the **markdown** body for issue 2.\n\nIt has multiple paragraphs.",
|
|
state: nil,
|
|
)
|
|
}
|
|
}
|
|
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "add testbot collaborator") {
|
|
try await repoService.addCollaborator(
|
|
owner: adminUser, repo: "test-repo",
|
|
username: testbotUser, permission: "write",
|
|
)
|
|
}
|
|
}
|
|
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "add readonly collaborator") {
|
|
try await repoService.addCollaborator(
|
|
owner: adminUser, repo: "test-repo",
|
|
username: readonlyUser, permission: "read",
|
|
)
|
|
}
|
|
}
|
|
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "create label") {
|
|
_ = try await repoService.createLabel(
|
|
owner: adminUser, repo: "test-repo",
|
|
name: "bug", color: "#ee0701",
|
|
)
|
|
}
|
|
}
|
|
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "create milestone") {
|
|
_ = try await repoService.createMilestone(
|
|
owner: adminUser, repo: "test-repo",
|
|
title: "v1.0", description: "First release milestone",
|
|
dueOn: "2026-03-01T00:00:00Z",
|
|
)
|
|
}
|
|
}
|
|
|
|
try await group.waitForAll()
|
|
}
|
|
|
|
_ = try await repoService.createFile(
|
|
owner: adminUser, repo: "test-repo", path: "hello.py",
|
|
content: "print(\"Hello from Forgejo!\")",
|
|
message: "Add hello.py",
|
|
)
|
|
|
|
_ = try await repoService.createFile(
|
|
owner: adminUser, repo: "test-repo", path: "src/main.py",
|
|
content: "def main():\n print(\"Main module\")",
|
|
message: "Add src/main.py",
|
|
)
|
|
|
|
try await paginationTask
|
|
|
|
print("Phase 3 done.")
|
|
|
|
print("Setting issue metadata...")
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "add label to issue #1") {
|
|
_ = try await adminIssueService.replaceLabels(
|
|
owner: adminUser, repo: "test-repo",
|
|
index: 1, labelIDs: [1],
|
|
)
|
|
}
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "set milestone on issue #1") {
|
|
_ = try await adminIssueService.editIssue(
|
|
owner: adminUser, repo: "test-repo", index: 1,
|
|
title: nil, body: nil, state: nil,
|
|
milestone: 1, assignees: [adminUser],
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Phase 4: PRs
|
|
|
|
private func createPullRequests(testbotClient: ForgejoClient) async throws {
|
|
let repoService = RepositoryService(client: testbotClient)
|
|
let prService = PullRequestService(client: testbotClient)
|
|
|
|
print("Creating feature branch...")
|
|
_ = try await repoService.createFile(
|
|
owner: adminUser, repo: "test-repo", path: "feature.txt",
|
|
content: "This file was added in a feature branch",
|
|
message: "Add feature.txt",
|
|
newBranch: "feature-branch",
|
|
)
|
|
|
|
print("Creating pull request #4...")
|
|
_ = try await prService.createPullRequest(
|
|
owner: adminUser, repo: "test-repo",
|
|
title: "Add feature file", head: "feature-branch", base: "main",
|
|
body: "This PR adds a new feature file.",
|
|
)
|
|
|
|
print("Creating merge branch...")
|
|
_ = try await repoService.createFile(
|
|
owner: adminUser, repo: "test-repo", path: "merge-test.txt",
|
|
content: "This file is for merge testing",
|
|
message: "Add merge-test.txt",
|
|
newBranch: "merge-branch",
|
|
)
|
|
|
|
print("Creating pull request #5...")
|
|
_ = try await prService.createPullRequest(
|
|
owner: adminUser, repo: "test-repo",
|
|
title: "Merge test PR", head: "merge-branch", base: "main",
|
|
body: "This PR is for testing the merge flow.",
|
|
)
|
|
|
|
print("Creating squash-merge branch...")
|
|
_ = try await repoService.createFile(
|
|
owner: adminUser, repo: "test-repo", path: "squash-test.txt",
|
|
content: "This file is for squash merge testing",
|
|
message: "Add squash-test.txt",
|
|
newBranch: "squash-branch",
|
|
)
|
|
|
|
print("Creating pull request #6...")
|
|
_ = try await prService.createPullRequest(
|
|
owner: adminUser, repo: "test-repo",
|
|
title: "Squash merge test PR", head: "squash-branch", base: "main",
|
|
body: "This PR is for testing the squash merge flow.",
|
|
)
|
|
|
|
print("Creating rebase branch...")
|
|
_ = try await repoService.createFile(
|
|
owner: adminUser, repo: "test-repo", path: "rebase-test.txt",
|
|
content: "This file is for rebase testing",
|
|
message: "Add rebase-test.txt",
|
|
newBranch: "rebase-branch",
|
|
)
|
|
|
|
print("Creating pull request #7...")
|
|
_ = try await prService.createPullRequest(
|
|
owner: adminUser, repo: "test-repo",
|
|
title: "Rebase merge test PR", head: "rebase-branch", base: "main",
|
|
body: "This PR is for testing the rebase merge flow.",
|
|
)
|
|
}
|
|
|
|
// MARK: - Phase 5: PR metadata
|
|
|
|
private func setPRMetadata(
|
|
adminClient: ForgejoClient, testbotClient: ForgejoClient,
|
|
) async throws {
|
|
let testbotIssueService = IssueService(client: testbotClient)
|
|
let adminPRService = PullRequestService(client: adminClient)
|
|
let adminIssueService = IssueService(client: adminClient)
|
|
|
|
print("Setting PR metadata...")
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "comment on PR #4") {
|
|
_ = try await testbotIssueService.createComment(
|
|
owner: adminUser, repo: "test-repo",
|
|
index: 4, body: "Please review this PR when you get a chance.",
|
|
)
|
|
}
|
|
}
|
|
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "review PR #4") {
|
|
_ = try await adminPRService.createReview(
|
|
owner: adminUser, repo: "test-repo", index: 4,
|
|
body: "Looks good so far, just a few comments.",
|
|
event: "COMMENT",
|
|
comments: [
|
|
CreateReviewComment(
|
|
body: "Consider a more descriptive filename.",
|
|
path: "feature.txt",
|
|
oldPosition: nil,
|
|
newPosition: 1,
|
|
),
|
|
],
|
|
)
|
|
}
|
|
}
|
|
|
|
group.addTask {
|
|
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "set PR #4 metadata") {
|
|
_ = try await adminIssueService.editIssue(
|
|
owner: adminUser, repo: "test-repo", index: 4,
|
|
title: nil, body: nil, state: nil,
|
|
milestone: 1, assignees: [adminUser],
|
|
)
|
|
}
|
|
}
|
|
|
|
try await group.waitForAll()
|
|
}
|
|
}
|
|
|
|
// MARK: - Phase 6: API token
|
|
|
|
private func createAPIToken(adminClient: ForgejoClient) async throws {
|
|
let userService = UserService(client: adminClient)
|
|
|
|
print("Creating API token for testadmin...")
|
|
let token = try await userService.createToken(
|
|
username: adminUser,
|
|
name: "integration-test-token",
|
|
scopes: [
|
|
"read:user", "write:user",
|
|
"read:repository", "write:repository",
|
|
"read:issue", "write:issue",
|
|
"read:notification", "write:notification",
|
|
"read:organization",
|
|
],
|
|
)
|
|
|
|
let tokenPath = "/tmp/forgejo_test_token.txt"
|
|
try token.sha1.write(toFile: tokenPath, atomically: true, encoding: .utf8)
|
|
print("API token written to \(tokenPath)")
|
|
}
|
|
}
|
|
|
|
// MARK: - HTML README content
|
|
|
|
private let htmlReadmeContent = """
|
|
# HTML Readme Test
|
|
|
|
Regular markdown paragraph.
|
|
|
|
<details>
|
|
<summary>Click to expand</summary>
|
|
|
|
This content is hidden by default.
|
|
|
|
- Item A
|
|
- Item B
|
|
</details>
|
|
|
|
## Table
|
|
|
|
<table>
|
|
<tr><th>Name</th><th>Value</th></tr>
|
|
<tr><td>Alpha</td><td>1</td></tr>
|
|
<tr><td>Beta</td><td>2</td></tr>
|
|
</table>
|
|
|
|
## Keyboard Shortcuts
|
|
|
|
Press <kbd>Ctrl</kbd>+<kbd>C</kbd> to copy.
|
|
|
|
## Image
|
|
|
|
<img src="https://via.placeholder.com/150" alt="placeholder" width="150">
|
|
|
|
## Security Test
|
|
|
|
<script>alert("xss")</script>
|
|
|
|
<img src="x" onerror="alert('xss')">
|
|
|
|
<iframe src="https://evil.example.com"></iframe>
|
|
|
|
<div onclick="alert('xss')">Click me</div>
|
|
"""
|