test: switch from bash to swift for integration test setup

This commit is contained in:
Stefan Hausotte 2026-03-11 22:44:19 +01:00
parent 0505a03bc8
commit 6596f6116d
11 changed files with 561 additions and 330 deletions

View file

@ -639,7 +639,7 @@
repositoryURL = "https://codeberg.org/secana/ForgejoKit.git";
requirement = {
kind = exactVersion;
version = 0.1.0;
version = 0.2.0;
};
};
DEC49F6B2F3D00C700E7DD54 /* XCRemoteSwiftPackageReference "textual" */ = {

View file

@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://codeberg.org/secana/ForgejoKit.git",
"state" : {
"revision" : "897a8ebeddfc97444c27752df8e99eda589eeaba",
"version" : "0.1.0"
"revision" : "a73f04462372ec2976e0ef673dd1ee17c3a7239d",
"version" : "0.2.0"
}
},
{

View file

@ -0,0 +1,15 @@
{
"originHash" : "6925739b095ee90912b50df5d7f72a20c934e3a08983bcca294a8831f90689cd",
"pins" : [
{
"identity" : "forgejokit",
"kind" : "remoteSourceControl",
"location" : "https://codeberg.org/secana/ForgejoKit.git",
"state" : {
"revision" : "a73f04462372ec2976e0ef673dd1ee17c3a7239d",
"version" : "0.2.0"
}
}
],
"version" : 3
}

View file

@ -0,0 +1,20 @@
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "forgejo-seed",
platforms: [
.macOS(.v15),
],
dependencies: [
.package(url: "https://codeberg.org/secana/ForgejoKit.git", from: "0.2.0"),
],
targets: [
.executableTarget(
name: "forgejo-seed",
dependencies: ["ForgejoKit"],
path: "Sources"
),
]
)

View file

@ -0,0 +1,32 @@
import Foundation
enum DockerExec {
static func run(
composeFile: String, service: String,
command: [String],
) throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = [
"docker", "compose", "-f", composeFile,
"exec", "-T", "-u", "git", service,
] + command
let stderrPipe = Pipe()
process.standardError = stderrPipe
process.standardOutput = FileHandle.nullDevice
try process.run()
process.waitUntilExit()
if process.terminationStatus != 0 {
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
let stderr = String(data: stderrData, encoding: .utf8) ?? ""
throw SeedError.dockerExecFailed(
command: command.joined(separator: " "),
exitCode: process.terminationStatus,
stderr: stderr,
)
}
}
}

View file

@ -0,0 +1,30 @@
import Foundation
@main
struct ForgejeSeed {
static func main() async throws {
let args = CommandLine.arguments
guard args.count >= 3 else {
throw SeedError.missingArguments
}
let baseURL = args[1]
let serviceName = args[2]
// Compose file path: passed as 3rd arg, or derived from cwd
let composeFile: String = if args.count >= 4 {
args[3]
} else {
FileManager.default.currentDirectoryPath
+ "/integration/docker-compose.yml"
}
let seeder = Seeder(
baseURL: baseURL,
serviceName: serviceName,
composeFile: composeFile,
)
try await seeder.seed()
}
}

View file

@ -0,0 +1,20 @@
import Foundation
func withRetry<T>(
maxAttempts: Int, delay: Duration,
operation: String,
body: () async throws -> T,
) async throws -> T {
var lastError: any Error = SeedError.retryExhausted(operation: operation, attempts: maxAttempts)
for attempt in 1 ... maxAttempts {
do {
return try await body()
} catch {
lastError = error
if attempt == maxAttempts { break }
print(" \(operation) attempt \(attempt) failed, retrying...")
try await Task.sleep(for: delay)
}
}
throw lastError
}

View file

@ -0,0 +1,18 @@
import Foundation
enum SeedError: LocalizedError {
case missingArguments
case dockerExecFailed(command: String, exitCode: Int32, stderr: String)
case retryExhausted(operation: String, attempts: Int)
var errorDescription: String? {
switch self {
case .missingArguments:
"Usage: forgejo-seed <base-url> <service-name> [compose-file]"
case let .dockerExecFailed(command, exitCode, stderr):
"Docker exec failed (\(exitCode)): \(command)\n\(stderr)"
case let .retryExhausted(operation, attempts):
"\(operation) failed after \(attempts) attempts"
}
}
}

View file

@ -0,0 +1,407 @@
// 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,
)
try createAdminUser()
try await createAPIUsers(adminClient: adminClient)
try await createRepositories(adminClient: adminClient)
try await createIssuesFilesMetadata(adminClient: adminClient, testbotClient: testbotClient)
try await createPullRequests(testbotClient: testbotClient)
try await setPRMetadata(adminClient: adminClient, testbotClient: testbotClient)
try await createAPIToken(adminClient: adminClient)
print("\nSeed data created successfully.")
}
// 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,
)
}
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.",
)
}
// 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>
"""

View file

@ -1,310 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
SCRIPT_DIR="$(dirname "$0")"
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml"
BASE_URL="${1:-http://localhost:13001}"
SERVICE_NAME="${2:-forgejo-1}"
ADMIN_USER="testadmin"
ADMIN_PASS="admin1234"
ADMIN_AUTH="$ADMIN_USER:$ADMIN_PASS"
TESTBOT_AUTH="testbot:testbot1234"
# --- Phase 1: Create users (sequential — prerequisite for everything) ---
echo "Creating admin user..."
docker compose -f "$COMPOSE_FILE" exec -T -u git "$SERVICE_NAME" \
forgejo admin user create --admin \
--username "$ADMIN_USER" \
--password "$ADMIN_PASS" \
--email testadmin@test.local \
--must-change-password=false
echo "Creating testbot user..."
docker compose -f "$COMPOSE_FILE" exec -T -u git "$SERVICE_NAME" \
forgejo admin user create \
--username testbot \
--password testbot1234 \
--email testbot@test.local \
--must-change-password=false
echo "Creating readonlyuser..."
docker compose -f "$COMPOSE_FILE" exec -T -u git "$SERVICE_NAME" \
forgejo admin user create \
--username readonlyuser \
--password readonly1234 \
--email readonlyuser@test.local \
--must-change-password=false
# --- Phase 2: Create repositories (parallel) ---
echo "Creating repositories..."
curl -sf -X POST "$BASE_URL/api/v1/user/repos" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"name": "test-repo", "private": false, "auto_init": true}' &
curl -sf -X POST "$BASE_URL/api/v1/user/repos" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"name": "test-repo-2", "description": "A second test repository", "private": false, "auto_init": true}' &
curl -sf -X POST "$BASE_URL/api/v1/user/repos" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"name": "archived-repo", "description": "An archived repository", "private": false, "auto_init": true}' &
curl -sf -X POST "$BASE_URL/api/v1/user/repos" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"name": "html-readme-repo", "description": "Repo with HTML in README", "private": false, "auto_init": true}' &
wait
echo "Repositories created."
# Archive the archived-repo (retry — the repo may not be ready immediately after creation)
echo "Archiving archived-repo..."
archived=false
for attempt in 1 2 3 4 5; do
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/archived-repo" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"archived": true}')
if [ "$http_code" = "200" ]; then
archived=true
break
fi
echo " Archive attempt $attempt failed (HTTP $http_code), retrying..."
sleep 2
done
if [ "$archived" != "true" ]; then
echo "ERROR: Failed to archive repo after 5 attempts" >&2
exit 1
fi
# Update html-readme-repo README with HTML content
echo "Updating html-readme-repo README..."
README_SHA=""
for attempt in 1 2 3 4 5; do
response=$(curl -sf "$BASE_URL/api/v1/repos/$ADMIN_USER/html-readme-repo/contents/README.md" \
-u "$ADMIN_AUTH" 2>/dev/null) || { echo " README fetch attempt $attempt failed, retrying..."; sleep 2; continue; }
README_SHA=$(echo "$response" | python3 -c "import sys,json; print(json.load(sys.stdin)['sha'])" 2>/dev/null) && break
echo " README parse attempt $attempt failed, retrying..."
sleep 2
done
if [ -z "$README_SHA" ]; then
echo "ERROR: Failed to fetch README SHA after 5 attempts" >&2
exit 1
fi
README_PAYLOAD=$(python3 -c "
import base64, json
content = '''# 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>
'''
b64 = base64.b64encode(content.encode()).decode()
print(json.dumps({'content': b64, 'message': 'Add HTML to README', 'sha': '$README_SHA'}))
")
curl -sf -X PUT "$BASE_URL/api/v1/repos/$ADMIN_USER/html-readme-repo/contents/README.md" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d "$README_PAYLOAD"
# --- Phase 3: Create issues + code files + pagination issues (parallel) ---
# Create 25 pagination issues sequentially in a background subshell
# (sequential for deterministic issue numbering, background to overlap with other work)
echo "Creating pagination test issues..."
(for i in $(seq 1 25); do
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo-2/issues" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d "{\"title\": \"Pagination test issue $i\"}"
done) &
# Create issues 1-3 as testbot (generates notifications for testadmin)
echo "Creating issues as testbot..."
for i in 1 2 3; do
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues" \
-u "$TESTBOT_AUTH" \
-H "Content-Type: application/json" \
-d "{\"title\": \"Test issue $i from integration tests\"}"
done
# Phase 3b: Independent operations on test-repo (parallel)
echo "Setting up test-repo content..."
# Comment on issue #1
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/1/comments" \
-u "$TESTBOT_AUTH" \
-H "Content-Type: application/json" \
-d '{"body": "This is a test comment from testbot"}' &
# Add body text + close issue #3 in one PATCH
curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/3" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"body": "This is the **markdown** body for issue 3.\n\nIt has multiple paragraphs.", "state": "closed"}' &
# Body for issue #1
curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/1" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"body": "This is the **markdown** body for issue 1.\n\nIt has multiple paragraphs."}' &
# Body for issue #2
curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/2" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"body": "This is the **markdown** body for issue 2.\n\nIt has multiple paragraphs."}' &
# Code files (sequential — both commit to same branch)
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/contents/hello.py" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$(echo -n 'print(\"Hello from Forgejo!\")' | base64)\", \"message\": \"Add hello.py\"}"
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/contents/src/main.py" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$(echo -n 'def main():\n print(\"Main module\")' | base64)\", \"message\": \"Add src/main.py\"}"
# Add testbot as collaborator
curl -sf -X PUT "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/collaborators/testbot" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"permission": "write"}' &
# Add readonlyuser as read-only collaborator
curl -sf -X PUT "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/collaborators/readonlyuser" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"permission": "read"}' &
# Create label
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/labels" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"name": "bug", "color": "#ee0701"}' &
# Create milestone
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/milestones" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"title": "v1.0", "description": "First release milestone", "due_on": "2026-03-01T00:00:00Z"}' &
wait
echo "Phase 3 done."
# --- Phase 3c: Metadata that depends on label/milestone existing ---
# Label ID 1 and milestone ID 1 are deterministic because they're the first
# created on a fresh Forgejo instance (auto-increment starts at 1).
echo "Setting issue metadata..."
# Add label to issue #1 (label ID 1 = "bug", created in Phase 3b)
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/1/labels" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"labels": [1]}' &
# Set milestone + assignee on issue #1 (milestone ID 1 = "v1.0", created in Phase 3b)
curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/1" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"milestone": 1, "assignees": ["testadmin"]}' &
wait
# --- Phase 4: Create branches + PRs (sequential — numbers must be deterministic #4, #5) ---
echo "Creating feature branch..."
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/contents/feature.txt" \
-u "$TESTBOT_AUTH" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$(echo -n 'This file was added in a feature branch' | base64)\", \"message\": \"Add feature.txt\", \"new_branch\": \"feature-branch\"}"
echo "Creating pull request #4..."
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/pulls" \
-u "$TESTBOT_AUTH" \
-H "Content-Type: application/json" \
-d '{"title": "Add feature file", "head": "feature-branch", "base": "main", "body": "This PR adds a new feature file."}'
echo "Creating merge branch..."
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/contents/merge-test.txt" \
-u "$TESTBOT_AUTH" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$(echo -n 'This file is for merge testing' | base64)\", \"message\": \"Add merge-test.txt\", \"new_branch\": \"merge-branch\"}"
echo "Creating pull request #5..."
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/pulls" \
-u "$TESTBOT_AUTH" \
-H "Content-Type: application/json" \
-d '{"title": "Merge test PR", "head": "merge-branch", "base": "main", "body": "This PR is for testing the merge flow."}'
# --- Phase 5: Post-PR metadata (parallel) ---
echo "Setting PR metadata..."
# Comment on PR #4
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/4/comments" \
-u "$TESTBOT_AUTH" \
-H "Content-Type: application/json" \
-d '{"body": "Please review this PR when you get a chance."}' &
# Review on PR #4
curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/pulls/4/reviews" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"event": "COMMENT", "body": "Looks good so far, just a few comments.", "comments": [{"path": "feature.txt", "body": "Consider a more descriptive filename.", "new_position": 1}]}' &
# Milestone + assignee on PR #4
curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/4" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"milestone": 1, "assignees": ["testadmin"]}' &
wait
# --- Phase 6: Create API token for token-based auth tests ---
echo "Creating API token for testadmin..."
TOKEN_RESPONSE=$(curl -sf -X POST "$BASE_URL/api/v1/users/$ADMIN_USER/tokens" \
-u "$ADMIN_AUTH" \
-H "Content-Type: application/json" \
-d '{"name": "integration-test-token", "scopes": ["read:user", "write:user", "read:repository", "write:repository", "read:issue", "write:issue", "read:notification", "write:notification", "read:organization"]}')
API_TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['sha1'])")
echo "$API_TOKEN" > /tmp/forgejo_test_token.txt
echo "API token written to /tmp/forgejo_test_token.txt"
echo ""
echo "Seed data created successfully."

View file

@ -75,12 +75,14 @@ docker-up:
docker-down:
docker compose -f {{compose_file}} down -v 2>/dev/null || true
rm -f /tmp/forgejo_test_url.txt /tmp/forgejo_test_url_*.txt
rm -f /tmp/forgejo_test_token.txt /tmp/forgejo_test_token_*.txt
# Clear integration test caches (seed snapshots, temp files)
clean-integration:
rm -f integration/.forgejo-seed-snapshot.tar.gz integration/.forgejo-seed-hash
rm -f /tmp/forgejo_test_url.txt /tmp/forgejo_test_url_*.txt
rm -f /tmp/forgejo_test_token.txt /tmp/forgejo_test_token_*.txt
rm -rf integration/forgejo-seed/.build
@echo "Integration test caches cleared."
# Seed test data into Forgejo instances (with snapshot caching)
@ -92,7 +94,7 @@ seed:
BASE_URL_2="{{base_url_2}}"
SNAPSHOT_FILE="integration/.forgejo-seed-snapshot.tar.gz"
HASH_FILE="integration/.forgejo-seed-hash"
SETUP_HASH=$(shasum -a 256 integration/setup.sh | awk '{print $1}')
SETUP_HASH=$(find integration/forgejo-seed/Sources -name '*.swift' -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256 | awk '{print $1}')
wait_for_instance() {
local url="$1" elapsed=0
until curl -sf "$url/api/v1/version" > /dev/null 2>&1; do
@ -122,7 +124,7 @@ seed:
echo "Snapshot restored to both instances."
else
echo "Seeding test data..."
bash integration/setup.sh "$BASE_URL_1" forgejo-1
swift run --package-path integration/forgejo-seed forgejo-seed "$BASE_URL_1" forgejo-1
echo "Snapshotting seed data..."
CONTAINER_1=$(docker compose -f "$COMPOSE_FILE" ps -q forgejo-1)
docker exec "$CONTAINER_1" sh -c "cd / && tar czf /tmp/snapshot.tar.gz data"
@ -227,17 +229,12 @@ test-one filter="" destination=default_destination:
just docker-down
}
trap cleanup EXIT
SIM_NAME="$(echo '{{destination}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')"
xcrun simctl boot "$SIM_NAME" 2>/dev/null || true
just sim-boot '{{destination}}' '{{destination}}' &
just docker-up &
xcodebuild build-for-testing \
-project Forji/Forji.xcodeproj \
-scheme Forji \
-destination '{{destination}}' 2>&1 | xcbeautify &
just build-for-testing '{{destination}}' '{{destination}}' &
wait
just seed
SIM_SAFE="${SIM_NAME// /_}"
echo "{{base_url_1}}" > "/tmp/forgejo_test_url_${SIM_SAFE}.txt"
just _write-url-file '{{destination}}' '{{base_url_1}}'
echo "{{base_url_1}}" > /tmp/forgejo_test_url.txt
xcodebuild test-without-building \
-project Forji/Forji.xcodeproj \
@ -264,13 +261,15 @@ build-for-testing destination_a=default_destination destination_b=default_destin
-destination '{{destination_b}}' 2>&1 | xcbeautify
[private]
_write-url-files destination_a=default_destination destination_b=default_destination_b:
_write-url-file destination url:
#!/usr/bin/env bash
set -eo pipefail
SIM_A="$(echo '{{destination_a}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')"
SIM_B="$(echo '{{destination_b}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')"
SIM_A_SAFE="${SIM_A// /_}"
SIM_B_SAFE="${SIM_B// /_}"
echo "{{base_url_1}}" > "/tmp/forgejo_test_url_${SIM_A_SAFE}.txt"
echo "{{base_url_2}}" > "/tmp/forgejo_test_url_${SIM_B_SAFE}.txt"
SIM="$(echo '{{destination}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')"
SIM_SAFE="${SIM// /_}"
echo "{{url}}" > "/tmp/forgejo_test_url_${SIM_SAFE}.txt"
[private]
_write-url-files destination_a=default_destination destination_b=default_destination_b:
just _write-url-file '{{destination_a}}' '{{base_url_1}}'
just _write-url-file '{{destination_b}}' '{{base_url_2}}'
echo "{{base_url_1}}" > /tmp/forgejo_test_url.txt