mirror of
https://codeberg.org/secana/Forji.git
synced 2026-06-16 05:13:55 -07:00
test: switch from bash to swift for integration test setup
This commit is contained in:
parent
0505a03bc8
commit
6596f6116d
11 changed files with 561 additions and 330 deletions
|
|
@ -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" */ = {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
15
integration/forgejo-seed/Package.resolved
Normal file
15
integration/forgejo-seed/Package.resolved
Normal 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
|
||||
}
|
||||
20
integration/forgejo-seed/Package.swift
Normal file
20
integration/forgejo-seed/Package.swift
Normal 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"
|
||||
),
|
||||
]
|
||||
)
|
||||
32
integration/forgejo-seed/Sources/DockerExec.swift
Normal file
32
integration/forgejo-seed/Sources/DockerExec.swift
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
30
integration/forgejo-seed/Sources/Main.swift
Normal file
30
integration/forgejo-seed/Sources/Main.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
20
integration/forgejo-seed/Sources/Retry.swift
Normal file
20
integration/forgejo-seed/Sources/Retry.swift
Normal 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
|
||||
}
|
||||
18
integration/forgejo-seed/Sources/SeedError.swift
Normal file
18
integration/forgejo-seed/Sources/SeedError.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
407
integration/forgejo-seed/Sources/Seeder.swift
Normal file
407
integration/forgejo-seed/Sources/Seeder.swift
Normal 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>
|
||||
"""
|
||||
|
|
@ -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."
|
||||
33
justfile
33
justfile
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue