mirror of
https://codeberg.org/secana/ForgejoKit.git
synced 2026-06-16 05:13:53 -07:00
chore: formatting and function names
This commit is contained in:
parent
b78c57ffd9
commit
81c74f6dc3
20 changed files with 192 additions and 191 deletions
|
|
@ -1,2 +1,3 @@
|
||||||
--swiftversion 6.2
|
--swiftversion 6.2
|
||||||
--disable redundantMemberwiseInit
|
--disable redundantMemberwiseInit
|
||||||
|
--disable swiftTestingTestCaseNames
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Foundation
|
||||||
/// Represents a single Forgejo Actions run.
|
/// Represents a single Forgejo Actions run.
|
||||||
///
|
///
|
||||||
/// Mirrors the Forgejo `ActionRun` schema. Note: Forgejo merges status and
|
/// Mirrors the Forgejo `ActionRun` schema. Note: Forgejo merges status and
|
||||||
/// conclusion into a single `status` field — values are
|
/// conclusion into a single `status` field, values are
|
||||||
/// `success`, `failure`, `cancelled`, `skipped`, `running`, `waiting`,
|
/// `success`, `failure`, `cancelled`, `skipped`, `running`, `waiting`,
|
||||||
/// `blocked`, `unknown`.
|
/// `blocked`, `unknown`.
|
||||||
public struct WorkflowRun: Codable, Identifiable, Sendable {
|
public struct WorkflowRun: Codable, Identifiable, Sendable {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Foundation
|
||||||
/// Response from Forgejo's internal web route
|
/// Response from Forgejo's internal web route
|
||||||
/// `POST /{owner}/{repo}/actions/runs/{runIndex}/jobs/{jobIndex}`.
|
/// `POST /{owner}/{repo}/actions/runs/{runIndex}/jobs/{jobIndex}`.
|
||||||
///
|
///
|
||||||
/// **Experimental.** This endpoint is the web UI's own JSON contract — it is
|
/// **Experimental.** This endpoint is the web UI's own JSON contract, it is
|
||||||
/// not part of `/api/v1` and may change between Forgejo releases without
|
/// not part of `/api/v1` and may change between Forgejo releases without
|
||||||
/// notice. Use only for surfaces where graceful degradation is acceptable.
|
/// notice. Use only for surfaces where graceful degradation is acceptable.
|
||||||
public struct WorkflowRunView: Codable, Sendable {
|
public struct WorkflowRunView: Codable, Sendable {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Foundation
|
||||||
public class URLSessionManager: NSObject, @unchecked Sendable {
|
public class URLSessionManager: NSObject, @unchecked Sendable {
|
||||||
public let allowSelfSignedCertificates: Bool
|
public let allowSelfSignedCertificates: Bool
|
||||||
private let trustedHost: String?
|
private let trustedHost: String?
|
||||||
// Initialized eagerly in init — safe for concurrent access as it's only written once.
|
// Initialized eagerly in init, safe for concurrent access as it's only written once.
|
||||||
private var _session: URLSession!
|
private var _session: URLSession!
|
||||||
public var session: URLSession {
|
public var session: URLSession {
|
||||||
_session
|
_session
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ public final class ForgejoClient: Sendable {
|
||||||
else { throw AuthenticationError.invalidResponse }
|
else { throw AuthenticationError.invalidResponse }
|
||||||
return sha1
|
return sha1
|
||||||
case 400, 422:
|
case 400, 422:
|
||||||
// Token name already taken — Forgejo returns 400, some versions use 422
|
// Token name already taken, Forgejo returns 400, some versions use 422
|
||||||
let body = String(data: data, encoding: .utf8) ?? ""
|
let body = String(data: data, encoding: .utf8) ?? ""
|
||||||
guard body.contains("name") else {
|
guard body.contains("name") else {
|
||||||
throw AuthenticationError.from(statusCode: httpResponse.statusCode, body: body)
|
throw AuthenticationError.from(statusCode: httpResponse.statusCode, body: body)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Forgejo Actions API. Forgejo's Actions API is much smaller than Gitea's
|
/// Forgejo Actions API. Forgejo's Actions API is much smaller than Gitea's
|
||||||
/// or GitHub's — there is no public endpoint to list workflow definitions,
|
/// or GitHub's, there is no public endpoint to list workflow definitions,
|
||||||
/// fetch jobs of a run, or stream job logs. Only run listing and run detail
|
/// fetch jobs of a run, or stream job logs. Only run listing and run detail
|
||||||
/// are exposed.
|
/// are exposed.
|
||||||
public final class WorkflowService: Sendable {
|
public final class WorkflowService: Sendable {
|
||||||
|
|
@ -114,7 +114,7 @@ public final class WorkflowService: Sendable {
|
||||||
)
|
)
|
||||||
// Forgejo's POST handler at this base path uses `attempt=0` which fails
|
// Forgejo's POST handler at this base path uses `attempt=0` which fails
|
||||||
// for any run whose tasks are stored with the (1-based) attempt number.
|
// for any run whose tasks are stored with the (1-based) attempt number.
|
||||||
// The matching GET endpoint 307-redirects to the latest-attempt URL —
|
// The matching GET endpoint 307-redirects to the latest-attempt URL,
|
||||||
// resolve that first, then POST to the resolved attempt-suffixed URL.
|
// resolve that first, then POST to the resolved attempt-suffixed URL.
|
||||||
let postURL = await (try? client.discoverRedirectLocation(url: baseURL)) ?? baseURL
|
let postURL = await (try? client.discoverRedirectLocation(url: baseURL)) ?? baseURL
|
||||||
let body = try client.encodeRequestBody(ViewRequestBody(logCursors: logCursors))
|
let body = try client.encodeRequestBody(ViewRequestBody(logCursors: logCursors))
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
struct AdminServiceTests {
|
struct AdminServiceTests {
|
||||||
@Test func `create user payload encodes correct keys`() throws {
|
@Test func createUserPayloadEncodesCorrectKeys() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"username": "testbot",
|
"username": "testbot",
|
||||||
|
|
@ -27,7 +27,7 @@ struct AdminServiceTests {
|
||||||
#expect(dict["visibility"] as? String == "public")
|
#expect(dict["visibility"] as? String == "public")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `create user URL construction`() throws {
|
@Test func createUserURLConstruction() throws {
|
||||||
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "admin", password: "pass")
|
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "admin", password: "pass")
|
||||||
let url = try client.makeURL(path: "/api/v1/admin/users")
|
let url = try client.makeURL(path: "/api/v1/admin/users")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/admin/users")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/admin/users")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ struct AttachmentTests {
|
||||||
|
|
||||||
// MARK: - Model decoding
|
// MARK: - Model decoding
|
||||||
|
|
||||||
@Test func `decodes attachment`() throws {
|
@Test func decodesAttachment() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -33,8 +33,8 @@ struct AttachmentTests {
|
||||||
#expect(attachment.isImage == true)
|
#expect(attachment.isImage == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes attachment without type field`() throws {
|
@Test func decodesAttachmentWithoutTypeField() throws {
|
||||||
// Forgejo API does not always include a type field — isImage falls back to file extension.
|
// Forgejo API does not always include a type field, isImage falls back to file extension.
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -50,7 +50,7 @@ struct AttachmentTests {
|
||||||
#expect(attachment.isImage == true)
|
#expect(attachment.isImage == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes non image attachment`() throws {
|
@Test func decodesNonImageAttachment() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -68,7 +68,7 @@ struct AttachmentTests {
|
||||||
#expect(attachment.isImage == false)
|
#expect(attachment.isImage == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue with assets`() throws {
|
@Test func decodesIssueWithAssets() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -99,7 +99,7 @@ struct AttachmentTests {
|
||||||
#expect(issue.assets?.first?.isImage == true)
|
#expect(issue.assets?.first?.isImage == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue with no assets`() throws {
|
@Test func decodesIssueWithNoAssets() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -117,7 +117,7 @@ struct AttachmentTests {
|
||||||
#expect(issue.assets == nil)
|
#expect(issue.assets == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue comment with assets`() throws {
|
@Test func decodesIssueCommentWithAssets() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -145,7 +145,7 @@ struct AttachmentTests {
|
||||||
|
|
||||||
// MARK: - isImage
|
// MARK: - isImage
|
||||||
|
|
||||||
@Test func `is image for various mime types`() {
|
@Test func isImageForVariousMimeTypes() {
|
||||||
let makeAttachment = { (type: String) in
|
let makeAttachment = { (type: String) in
|
||||||
Attachment(
|
Attachment(
|
||||||
id: 1, uuid: "u", name: "f.bin", size: 0,
|
id: 1, uuid: "u", name: "f.bin", size: 0,
|
||||||
|
|
@ -162,7 +162,7 @@ struct AttachmentTests {
|
||||||
#expect(makeAttachment("application/zip").isImage == false)
|
#expect(makeAttachment("application/zip").isImage == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `is image falls back to file extension when type is nil`() {
|
@Test func isImageFallsBackToFileExtensionWhenTypeIsNil() {
|
||||||
let makeAttachment = { (name: String) in
|
let makeAttachment = { (name: String) in
|
||||||
Attachment(
|
Attachment(
|
||||||
id: 1, uuid: "u", name: name, size: 0,
|
id: 1, uuid: "u", name: name, size: 0,
|
||||||
|
|
@ -180,7 +180,7 @@ struct AttachmentTests {
|
||||||
#expect(makeAttachment("readme.txt").isImage == false)
|
#expect(makeAttachment("readme.txt").isImage == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `is image falls back to extension when type is not a mime type`() {
|
@Test func isImageFallsBackToExtensionWhenTypeIsNotAMimeType() {
|
||||||
// Forgejo returns "type": "attachment" for all uploads, not an actual MIME type.
|
// Forgejo returns "type": "attachment" for all uploads, not an actual MIME type.
|
||||||
// isImage must fall through to the extension check in this case.
|
// isImage must fall through to the extension check in this case.
|
||||||
let makeAttachment = { (name: String) in
|
let makeAttachment = { (name: String) in
|
||||||
|
|
@ -199,19 +199,19 @@ struct AttachmentTests {
|
||||||
|
|
||||||
// MARK: - Upload URL construction
|
// MARK: - Upload URL construction
|
||||||
|
|
||||||
@Test func `upload issue attachment URL`() throws {
|
@Test func uploadIssueAttachmentURL() throws {
|
||||||
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "u", password: "p")
|
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "u", password: "p")
|
||||||
let url = try client.makeRepoURL(owner: "alice", repo: "myrepo", path: "/issues/42/assets")
|
let url = try client.makeRepoURL(owner: "alice", repo: "myrepo", path: "/issues/42/assets")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/alice/myrepo/issues/42/assets")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/alice/myrepo/issues/42/assets")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `upload comment attachment URL`() throws {
|
@Test func uploadCommentAttachmentURL() throws {
|
||||||
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "u", password: "p")
|
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "u", password: "p")
|
||||||
let url = try client.makeRepoURL(owner: "alice", repo: "myrepo", path: "/issues/comments/99/assets")
|
let url = try client.makeRepoURL(owner: "alice", repo: "myrepo", path: "/issues/comments/99/assets")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/alice/myrepo/issues/comments/99/assets")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/alice/myrepo/issues/comments/99/assets")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `upload URL encodes owner and repo`() throws {
|
@Test func uploadURLEncodesOwnerAndRepo() throws {
|
||||||
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "u", password: "p")
|
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "u", password: "p")
|
||||||
let url = try client.makeRepoURL(owner: "my org", repo: "my repo", path: "/issues/1/assets")
|
let url = try client.makeRepoURL(owner: "my org", repo: "my repo", path: "/issues/1/assets")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/my%20org/my%20repo/issues/1/assets")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/my%20org/my%20repo/issues/1/assets")
|
||||||
|
|
@ -219,7 +219,7 @@ struct AttachmentTests {
|
||||||
|
|
||||||
// MARK: - Multipart body
|
// MARK: - Multipart body
|
||||||
|
|
||||||
@Test func `build multipart body structure`() {
|
@Test func buildMultipartBodyStructure() {
|
||||||
let fileData = Data("hello".utf8)
|
let fileData = Data("hello".utf8)
|
||||||
let boundary = "test-boundary-123"
|
let boundary = "test-boundary-123"
|
||||||
let body = ForgejoClient.buildMultipartBody(
|
let body = ForgejoClient.buildMultipartBody(
|
||||||
|
|
@ -236,7 +236,7 @@ struct AttachmentTests {
|
||||||
#expect(bodyString.hasSuffix("--\(boundary)--\r\n"))
|
#expect(bodyString.hasSuffix("--\(boundary)--\r\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `build multipart body contains file data`() {
|
@Test func buildMultipartBodyContainsFileData() {
|
||||||
let fileData = Data([0xFF, 0xD8, 0xFF, 0xE0]) // JPEG header bytes
|
let fileData = Data([0xFF, 0xD8, 0xFF, 0xE0]) // JPEG header bytes
|
||||||
let boundary = "b"
|
let boundary = "b"
|
||||||
let body = ForgejoClient.buildMultipartBody(
|
let body = ForgejoClient.buildMultipartBody(
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,21 @@ import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
struct BearerCredentialTests {
|
struct BearerCredentialTests {
|
||||||
@Test func `bearer credential uses bearer scheme`() {
|
@Test func bearerCredentialUsesBearerScheme() {
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://codeberg.org", username: "octocat", bearerToken: "abc123",
|
serverURL: "https://codeberg.org", username: "octocat", bearerToken: "abc123",
|
||||||
)
|
)
|
||||||
#expect(client.authorizationHeaderValue == "Bearer abc123")
|
#expect(client.authorizationHeaderValue == "Bearer abc123")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `token credential still uses token scheme`() {
|
@Test func tokenCredentialStillUsesTokenScheme() {
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://codeberg.org", username: "octocat", token: "abc123",
|
serverURL: "https://codeberg.org", username: "octocat", token: "abc123",
|
||||||
)
|
)
|
||||||
#expect(client.authorizationHeaderValue == "token abc123")
|
#expect(client.authorizationHeaderValue == "token abc123")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `basic credential uses basic scheme`() {
|
@Test func basicCredentialUsesBasicScheme() {
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://codeberg.org", username: "octocat", password: "pw",
|
serverURL: "https://codeberg.org", username: "octocat", password: "pw",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ struct DateDecodingTests {
|
||||||
|
|
||||||
// MARK: - ISO 8601 without fractional seconds
|
// MARK: - ISO 8601 without fractional seconds
|
||||||
|
|
||||||
@Test func `decodes date without fractional seconds`() throws {
|
@Test func decodesDateWithoutFractionalSeconds() throws {
|
||||||
let json = #"{"date":"2024-01-15T10:30:00Z"}"#
|
let json = #"{"date":"2024-01-15T10:30:00Z"}"#
|
||||||
let result = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
let result = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ struct DateDecodingTests {
|
||||||
|
|
||||||
// MARK: - ISO 8601 with fractional seconds
|
// MARK: - ISO 8601 with fractional seconds
|
||||||
|
|
||||||
@Test func `decodes date with fractional seconds`() throws {
|
@Test func decodesDateWithFractionalSeconds() throws {
|
||||||
let json = #"{"date":"2024-06-20T14:05:30.123Z"}"#
|
let json = #"{"date":"2024-06-20T14:05:30.123Z"}"#
|
||||||
let result = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
let result = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ struct DateDecodingTests {
|
||||||
#expect(utcCalendar.component(.hour, from: result.date) == 14)
|
#expect(utcCalendar.component(.hour, from: result.date) == 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes date with triple zero fraction`() throws {
|
@Test func decodesDateWithTripleZeroFraction() throws {
|
||||||
let json = #"{"date":"2024-01-15T10:30:00.000Z"}"#
|
let json = #"{"date":"2024-01-15T10:30:00.000Z"}"#
|
||||||
let result = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
let result = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
||||||
|
|
||||||
|
|
@ -53,14 +53,14 @@ struct DateDecodingTests {
|
||||||
|
|
||||||
// MARK: - Invalid dates
|
// MARK: - Invalid dates
|
||||||
|
|
||||||
@Test func `throws on invalid date string`() {
|
@Test func throwsOnInvalidDateString() {
|
||||||
let json = #"{"date":"not-a-date"}"#
|
let json = #"{"date":"not-a-date"}"#
|
||||||
#expect(throws: DecodingError.self) {
|
#expect(throws: DecodingError.self) {
|
||||||
_ = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
_ = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `throws on empty date string`() {
|
@Test func throwsOnEmptyDateString() {
|
||||||
let json = #"{"date":""}"#
|
let json = #"{"date":""}"#
|
||||||
#expect(throws: DecodingError.self) {
|
#expect(throws: DecodingError.self) {
|
||||||
_ = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
_ = try decoder().decode(DateWrapper.self, from: Data(json.utf8))
|
||||||
|
|
@ -69,7 +69,7 @@ struct DateDecodingTests {
|
||||||
|
|
||||||
// MARK: - Both formats decode to same value
|
// MARK: - Both formats decode to same value
|
||||||
|
|
||||||
@Test func `both formats produce same date`() throws {
|
@Test func bothFormatsProduceSameDate() throws {
|
||||||
let jsonWithout = #"{"date":"2024-03-10T08:00:00Z"}"#
|
let jsonWithout = #"{"date":"2024-03-10T08:00:00Z"}"#
|
||||||
let jsonWith = #"{"date":"2024-03-10T08:00:00.000Z"}"#
|
let jsonWith = #"{"date":"2024-03-10T08:00:00.000Z"}"#
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,19 @@ import Testing
|
||||||
struct DiffParserTests {
|
struct DiffParserTests {
|
||||||
// MARK: - Empty and minimal input
|
// MARK: - Empty and minimal input
|
||||||
|
|
||||||
@Test func `parse empty string`() {
|
@Test func parseEmptyString() {
|
||||||
let result = DiffParser.parse("")
|
let result = DiffParser.parse("")
|
||||||
#expect(result.files.isEmpty)
|
#expect(result.files.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `parse garbage input`() {
|
@Test func parseGarbageInput() {
|
||||||
let result = DiffParser.parse("this is not a diff")
|
let result = DiffParser.parse("this is not a diff")
|
||||||
#expect(result.files.isEmpty)
|
#expect(result.files.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Single file diff
|
// MARK: - Single file diff
|
||||||
|
|
||||||
@Test func `parse single file addition`() {
|
@Test func parseSingleFileAddition() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/hello.txt b/hello.txt",
|
"diff --git a/hello.txt b/hello.txt",
|
||||||
"new file mode 100644",
|
"new file mode 100644",
|
||||||
|
|
@ -45,7 +45,7 @@ struct DiffParserTests {
|
||||||
#expect(hunk.lines[3].type == .addition)
|
#expect(hunk.lines[3].type == .addition)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `parse single file deletion`() {
|
@Test func parseSingleFileDeletion() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/old.txt b/old.txt",
|
"diff --git a/old.txt b/old.txt",
|
||||||
"deleted file mode 100644",
|
"deleted file mode 100644",
|
||||||
|
|
@ -64,7 +64,7 @@ struct DiffParserTests {
|
||||||
#expect(file.newName == "/dev/null")
|
#expect(file.newName == "/dev/null")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `parse single file modification`() {
|
@Test func parseSingleFileModification() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/file.txt b/file.txt",
|
"diff --git a/file.txt b/file.txt",
|
||||||
"--- a/file.txt",
|
"--- a/file.txt",
|
||||||
|
|
@ -107,7 +107,7 @@ struct DiffParserTests {
|
||||||
|
|
||||||
// MARK: - Multiple files
|
// MARK: - Multiple files
|
||||||
|
|
||||||
@Test func `parse multiple files`() {
|
@Test func parseMultipleFiles() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/a.txt b/a.txt",
|
"diff --git a/a.txt b/a.txt",
|
||||||
"--- a/a.txt",
|
"--- a/a.txt",
|
||||||
|
|
@ -131,7 +131,7 @@ struct DiffParserTests {
|
||||||
|
|
||||||
// MARK: - Multiple hunks
|
// MARK: - Multiple hunks
|
||||||
|
|
||||||
@Test func `parse multiple hunks`() {
|
@Test func parseMultipleHunks() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/file.txt b/file.txt",
|
"diff --git a/file.txt b/file.txt",
|
||||||
"--- a/file.txt",
|
"--- a/file.txt",
|
||||||
|
|
@ -159,7 +159,7 @@ struct DiffParserTests {
|
||||||
|
|
||||||
// MARK: - Line number tracking
|
// MARK: - Line number tracking
|
||||||
|
|
||||||
@Test func `line numbers track correctly with multiple additions`() {
|
@Test func lineNumbersTrackCorrectlyWithMultipleAdditions() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/file.txt b/file.txt",
|
"diff --git a/file.txt b/file.txt",
|
||||||
"--- a/file.txt",
|
"--- a/file.txt",
|
||||||
|
|
@ -207,7 +207,7 @@ struct DiffParserTests {
|
||||||
|
|
||||||
// MARK: - Hunk header parsing
|
// MARK: - Hunk header parsing
|
||||||
|
|
||||||
@Test func `hunk header with function context`() {
|
@Test func hunkHeaderWithFunctionContext() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/main.swift b/main.swift",
|
"diff --git a/main.swift b/main.swift",
|
||||||
"--- a/main.swift",
|
"--- a/main.swift",
|
||||||
|
|
@ -226,7 +226,7 @@ struct DiffParserTests {
|
||||||
|
|
||||||
// MARK: - File name extraction
|
// MARK: - File name extraction
|
||||||
|
|
||||||
@Test func `file name extracted from minus plus`() {
|
@Test func fileNameExtractedFromMinusPlus() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/src/app.swift b/src/app.swift",
|
"diff --git a/src/app.swift b/src/app.swift",
|
||||||
"--- a/src/app.swift",
|
"--- a/src/app.swift",
|
||||||
|
|
@ -241,7 +241,7 @@ struct DiffParserTests {
|
||||||
#expect(result.files[0].newName == "src/app.swift")
|
#expect(result.files[0].newName == "src/app.swift")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `file name falls back to git header`() {
|
@Test func fileNameFallsBackToGitHeader() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/image.png b/image.png",
|
"diff --git a/image.png b/image.png",
|
||||||
"Binary files differ",
|
"Binary files differ",
|
||||||
|
|
@ -255,7 +255,7 @@ struct DiffParserTests {
|
||||||
|
|
||||||
// MARK: - Commit diff (new file from Forgejo API)
|
// MARK: - Commit diff (new file from Forgejo API)
|
||||||
|
|
||||||
@Test func `parse commit diff new file`() {
|
@Test func parseCommitDiffNewFile() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/src/main.py b/src/main.py",
|
"diff --git a/src/main.py b/src/main.py",
|
||||||
"new file mode 100644",
|
"new file mode 100644",
|
||||||
|
|
@ -281,7 +281,7 @@ struct DiffParserTests {
|
||||||
#expect(hunk.lines[1].oldLineNumber == nil)
|
#expect(hunk.lines[1].oldLineNumber == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `parse commit diff multiple files`() {
|
@Test func parseCommitDiffMultipleFiles() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/hello.py b/hello.py",
|
"diff --git a/hello.py b/hello.py",
|
||||||
"new file mode 100644",
|
"new file mode 100644",
|
||||||
|
|
@ -315,7 +315,7 @@ struct DiffParserTests {
|
||||||
|
|
||||||
// MARK: - No newline at end of file sentinel
|
// MARK: - No newline at end of file sentinel
|
||||||
|
|
||||||
@Test func `no newline at end of file sentinel is skipped`() {
|
@Test func noNewlineAtEndOfFileSentinelIsSkipped() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/file.txt b/file.txt",
|
"diff --git a/file.txt b/file.txt",
|
||||||
"--- a/file.txt",
|
"--- a/file.txt",
|
||||||
|
|
@ -344,7 +344,7 @@ struct DiffParserTests {
|
||||||
|
|
||||||
// MARK: - DiffLine identity
|
// MARK: - DiffLine identity
|
||||||
|
|
||||||
@Test func `each line has unique id`() {
|
@Test func eachLineHasUniqueId() {
|
||||||
let diff = [
|
let diff = [
|
||||||
"diff --git a/f.txt b/f.txt",
|
"diff --git a/f.txt b/f.txt",
|
||||||
"--- a/f.txt",
|
"--- a/f.txt",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
struct ErrorMappingTests {
|
struct ErrorMappingTests {
|
||||||
@Test func `classifies HTTP status codes`() {
|
@Test func classifiesHTTPStatusCodes() {
|
||||||
#expect(HTTPErrorCategory(statusCode: 401) == .authentication)
|
#expect(HTTPErrorCategory(statusCode: 401) == .authentication)
|
||||||
#expect(HTTPErrorCategory(statusCode: 403) == .permissionDenied)
|
#expect(HTTPErrorCategory(statusCode: 403) == .permissionDenied)
|
||||||
#expect(HTTPErrorCategory(statusCode: 404) == .notFound)
|
#expect(HTTPErrorCategory(statusCode: 404) == .notFound)
|
||||||
|
|
@ -11,7 +11,7 @@ struct ErrorMappingTests {
|
||||||
#expect(HTTPErrorCategory(statusCode: 400) == .other)
|
#expect(HTTPErrorCategory(statusCode: 400) == .other)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `exposes service error status and category`() {
|
@Test func exposesServiceErrorStatusAndCategory() {
|
||||||
let forbidden = ServiceError.httpError(statusCode: 403, message: "missing scope")
|
let forbidden = ServiceError.httpError(statusCode: 403, message: "missing scope")
|
||||||
#expect(forbidden.httpStatusCode == 403)
|
#expect(forbidden.httpStatusCode == 403)
|
||||||
#expect(forbidden.httpErrorCategory == .permissionDenied)
|
#expect(forbidden.httpErrorCategory == .permissionDenied)
|
||||||
|
|
@ -24,7 +24,7 @@ struct ErrorMappingTests {
|
||||||
#expect(ServiceError.invalidURL.httpErrorCategory == nil)
|
#expect(ServiceError.invalidURL.httpErrorCategory == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `maps authentication status codes`() {
|
@Test func mapsAuthenticationStatusCodes() {
|
||||||
#expect(AuthenticationError.from(statusCode: 401) == .invalidCredentials)
|
#expect(AuthenticationError.from(statusCode: 401) == .invalidCredentials)
|
||||||
#expect(AuthenticationError.from(statusCode: 401, body: "OTP required") == .otpRequired)
|
#expect(AuthenticationError.from(statusCode: 401, body: "OTP required") == .otpRequired)
|
||||||
#expect(AuthenticationError.from(statusCode: 403) == .unknownError(statusCode: 403))
|
#expect(AuthenticationError.from(statusCode: 403) == .unknownError(statusCode: 403))
|
||||||
|
|
@ -33,7 +33,7 @@ struct ErrorMappingTests {
|
||||||
#expect(AuthenticationError.from(statusCode: 418) == .unknownError(statusCode: 418))
|
#expect(AuthenticationError.from(statusCode: 418) == .unknownError(statusCode: 418))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `exposes authentication status and category`() {
|
@Test func exposesAuthenticationStatusAndCategory() {
|
||||||
#expect(AuthenticationError.invalidCredentials.httpStatusCode == 401)
|
#expect(AuthenticationError.invalidCredentials.httpStatusCode == 401)
|
||||||
#expect(AuthenticationError.invalidCredentials.httpErrorCategory == .authentication)
|
#expect(AuthenticationError.invalidCredentials.httpErrorCategory == .authentication)
|
||||||
#expect(AuthenticationError.serverNotFound.httpStatusCode == 404)
|
#expect(AuthenticationError.serverNotFound.httpStatusCode == 404)
|
||||||
|
|
@ -47,7 +47,7 @@ struct ErrorMappingTests {
|
||||||
|
|
||||||
// MARK: - ServiceError descriptions
|
// MARK: - ServiceError descriptions
|
||||||
|
|
||||||
@Test func `service error descriptions`() {
|
@Test func serviceErrorDescriptions() {
|
||||||
#expect(ServiceError.noActiveInstance.errorDescription == "No active Forgejo instance")
|
#expect(ServiceError.noActiveInstance.errorDescription == "No active Forgejo instance")
|
||||||
#expect(ServiceError.invalidURL.errorDescription == "Invalid URL")
|
#expect(ServiceError.invalidURL.errorDescription == "Invalid URL")
|
||||||
#expect(ServiceError.invalidResponse.errorDescription == "Invalid response from server")
|
#expect(ServiceError.invalidResponse.errorDescription == "Invalid response from server")
|
||||||
|
|
@ -56,7 +56,7 @@ struct ErrorMappingTests {
|
||||||
#expect(ServiceError.decodingFailed(detail: "missing key").errorDescription == "Decoding failed: missing key")
|
#expect(ServiceError.decodingFailed(detail: "missing key").errorDescription == "Decoding failed: missing key")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `http error description uses forgejo message body`() {
|
@Test func httpErrorDescriptionUsesForgejoMessageBody() {
|
||||||
let error = ServiceError.httpError(
|
let error = ServiceError.httpError(
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: #"{"message":"This pull request has failing status checks"}"#,
|
message: #"{"message":"This pull request has failing status checks"}"#,
|
||||||
|
|
@ -65,19 +65,19 @@ struct ErrorMappingTests {
|
||||||
#expect(error.errorDescription == "HTTP 405: This pull request has failing status checks")
|
#expect(error.errorDescription == "HTTP 405: This pull request has failing status checks")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `http error description falls back to raw body`() {
|
@Test func httpErrorDescriptionFallsBackToRawBody() {
|
||||||
let error = ServiceError.httpError(statusCode: 500, message: "database unavailable")
|
let error = ServiceError.httpError(statusCode: 500, message: "database unavailable")
|
||||||
|
|
||||||
#expect(error.errorDescription == "HTTP 500: database unavailable")
|
#expect(error.errorDescription == "HTTP 500: database unavailable")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `http error description ignores whitespace only message`() {
|
@Test func httpErrorDescriptionIgnoresWhitespaceOnlyMessage() {
|
||||||
let error = ServiceError.httpError(statusCode: 503, message: " \n ")
|
let error = ServiceError.httpError(statusCode: 503, message: " \n ")
|
||||||
|
|
||||||
#expect(error.errorDescription == "HTTP error: 503")
|
#expect(error.errorDescription == "HTTP error: 503")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `http error description falls back when JSON message field is empty`() {
|
@Test func httpErrorDescriptionFallsBackWhenJSONMessageFieldIsEmpty() {
|
||||||
let error = ServiceError.httpError(
|
let error = ServiceError.httpError(
|
||||||
statusCode: 422,
|
statusCode: 422,
|
||||||
message: #"{"message":" "}"#,
|
message: #"{"message":" "}"#,
|
||||||
|
|
@ -86,7 +86,7 @@ struct ErrorMappingTests {
|
||||||
#expect(error.errorDescription == "HTTP error: 422")
|
#expect(error.errorDescription == "HTTP error: 422")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `http error description returns raw JSON when message field missing`() {
|
@Test func httpErrorDescriptionReturnsRawJSONWhenMessageFieldMissing() {
|
||||||
let raw = #"{"errors":["bad request"]}"#
|
let raw = #"{"errors":["bad request"]}"#
|
||||||
let error = ServiceError.httpError(statusCode: 400, message: raw)
|
let error = ServiceError.httpError(statusCode: 400, message: raw)
|
||||||
|
|
||||||
|
|
@ -95,21 +95,21 @@ struct ErrorMappingTests {
|
||||||
|
|
||||||
// MARK: - Merge error collapsing
|
// MARK: - Merge error collapsing
|
||||||
|
|
||||||
@Test func `collapse merge error maps 405 with empty body to not mergeable`() {
|
@Test func collapseMergeErrorMaps405WithEmptyBodyToNotMergeable() {
|
||||||
let collapsed = PullRequestService.collapseMergeError(
|
let collapsed = PullRequestService.collapseMergeError(
|
||||||
.httpError(statusCode: 405, message: ""),
|
.httpError(statusCode: 405, message: ""),
|
||||||
)
|
)
|
||||||
#expect(collapsed == .notMergeable)
|
#expect(collapsed == .notMergeable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `collapse merge error maps 409 with empty body to merge conflict`() {
|
@Test func collapseMergeErrorMaps409WithEmptyBodyToMergeConflict() {
|
||||||
let collapsed = PullRequestService.collapseMergeError(
|
let collapsed = PullRequestService.collapseMergeError(
|
||||||
.httpError(statusCode: 409, message: " \n "),
|
.httpError(statusCode: 409, message: " \n "),
|
||||||
)
|
)
|
||||||
#expect(collapsed == .mergeConflict)
|
#expect(collapsed == .mergeConflict)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `collapse merge error preserves 405 with body`() {
|
@Test func collapseMergeErrorPreserves405WithBody() {
|
||||||
let original = ServiceError.httpError(
|
let original = ServiceError.httpError(
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: #"{"message":"failing status checks"}"#,
|
message: #"{"message":"failing status checks"}"#,
|
||||||
|
|
@ -117,17 +117,17 @@ struct ErrorMappingTests {
|
||||||
#expect(PullRequestService.collapseMergeError(original) == original)
|
#expect(PullRequestService.collapseMergeError(original) == original)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `collapse merge error preserves 409 with body`() {
|
@Test func collapseMergeErrorPreserves409WithBody() {
|
||||||
let original = ServiceError.httpError(statusCode: 409, message: "conflict in README.md")
|
let original = ServiceError.httpError(statusCode: 409, message: "conflict in README.md")
|
||||||
#expect(PullRequestService.collapseMergeError(original) == original)
|
#expect(PullRequestService.collapseMergeError(original) == original)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `collapse merge error passes other status codes through`() {
|
@Test func collapseMergeErrorPassesOtherStatusCodesThrough() {
|
||||||
let original = ServiceError.httpError(statusCode: 500, message: "")
|
let original = ServiceError.httpError(statusCode: 500, message: "")
|
||||||
#expect(PullRequestService.collapseMergeError(original) == original)
|
#expect(PullRequestService.collapseMergeError(original) == original)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `collapse merge error passes non HTTP errors through`() {
|
@Test func collapseMergeErrorPassesNonHTTPErrorsThrough() {
|
||||||
#expect(PullRequestService.collapseMergeError(.invalidURL) == .invalidURL)
|
#expect(PullRequestService.collapseMergeError(.invalidURL) == .invalidURL)
|
||||||
#expect(PullRequestService.collapseMergeError(.noActiveInstance) == .noActiveInstance)
|
#expect(PullRequestService.collapseMergeError(.noActiveInstance) == .noActiveInstance)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,14 @@ struct FileContentTests {
|
||||||
|
|
||||||
// MARK: - Base64 decoding
|
// MARK: - Base64 decoding
|
||||||
|
|
||||||
@Test func `decodes base 64 content`() {
|
@Test func decodesBase64Content() {
|
||||||
let original = "Hello, World!"
|
let original = "Hello, World!"
|
||||||
let base64 = Data(original.utf8).base64EncodedString()
|
let base64 = Data(original.utf8).base64EncodedString()
|
||||||
let file = makeFileContent(content: base64, encoding: "base64")
|
let file = makeFileContent(content: base64, encoding: "base64")
|
||||||
#expect(file.decodedContent == "Hello, World!")
|
#expect(file.decodedContent == "Hello, World!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes base 64 with newlines`() {
|
@Test func decodesBase64WithNewlines() {
|
||||||
let original = "Line one\nLine two\nLine three"
|
let original = "Line one\nLine two\nLine three"
|
||||||
let base64 = Data(original.utf8).base64EncodedString()
|
let base64 = Data(original.utf8).base64EncodedString()
|
||||||
let wrappedBase64 = String(base64.prefix(20)) + "\n" + String(base64.dropFirst(20))
|
let wrappedBase64 = String(base64.prefix(20)) + "\n" + String(base64.dropFirst(20))
|
||||||
|
|
@ -36,29 +36,29 @@ struct FileContentTests {
|
||||||
#expect(file.decodedContent == original)
|
#expect(file.decodedContent == original)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `returns nil content when content is nil`() {
|
@Test func returnsNilContentWhenContentIsNil() {
|
||||||
let file = makeFileContent(content: nil, encoding: "base64")
|
let file = makeFileContent(content: nil, encoding: "base64")
|
||||||
#expect(file.decodedContent == nil)
|
#expect(file.decodedContent == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `returns raw content when encoding is nil`() {
|
@Test func returnsRawContentWhenEncodingIsNil() {
|
||||||
let file = makeFileContent(content: "raw text", encoding: nil)
|
let file = makeFileContent(content: "raw text", encoding: nil)
|
||||||
#expect(file.decodedContent == "raw text")
|
#expect(file.decodedContent == "raw text")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `returns raw content when encoding is not base 64`() {
|
@Test func returnsRawContentWhenEncodingIsNotBase64() {
|
||||||
let file = makeFileContent(content: "raw text", encoding: "utf-8")
|
let file = makeFileContent(content: "raw text", encoding: "utf-8")
|
||||||
#expect(file.decodedContent == "raw text")
|
#expect(file.decodedContent == "raw text")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `returns nil for invalid base 64`() {
|
@Test func returnsNilForInvalidBase64() {
|
||||||
let file = makeFileContent(content: "!!!not-base64!!!", encoding: "base64")
|
let file = makeFileContent(content: "!!!not-base64!!!", encoding: "base64")
|
||||||
#expect(file.decodedContent == nil)
|
#expect(file.decodedContent == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - JSON decoding
|
// MARK: - JSON decoding
|
||||||
|
|
||||||
@Test func `decodes file content from JSON`() throws {
|
@Test func decodesFileContentFromJSON() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"name": "readme.md",
|
"name": "readme.md",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - User
|
// MARK: - User
|
||||||
|
|
||||||
@Test func `decodes user`() throws {
|
@Test func decodesUser() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -31,7 +31,7 @@ struct ModelDecodingTests {
|
||||||
#expect(user.isAdmin == true)
|
#expect(user.isAdmin == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes user with minimal fields`() throws {
|
@Test func decodesUserWithMinimalFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -47,7 +47,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - IssueLabel
|
// MARK: - IssueLabel
|
||||||
|
|
||||||
@Test func `decodes issue label`() throws {
|
@Test func decodesIssueLabel() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
|
|
@ -64,7 +64,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - IssueComment
|
// MARK: - IssueComment
|
||||||
|
|
||||||
@Test func `decodes issue comment`() throws {
|
@Test func decodesIssueComment() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 100,
|
"id": 100,
|
||||||
|
|
@ -82,7 +82,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - Issue
|
// MARK: - Issue
|
||||||
|
|
||||||
@Test func `decodes issue`() throws {
|
@Test func decodesIssue() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 50,
|
"id": 50,
|
||||||
|
|
@ -107,7 +107,7 @@ struct ModelDecodingTests {
|
||||||
#expect(issue.repository == nil)
|
#expect(issue.repository == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue with milestone and assignees`() throws {
|
@Test func decodesIssueWithMilestoneAndAssignees() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 56,
|
"id": 56,
|
||||||
|
|
@ -147,7 +147,7 @@ struct ModelDecodingTests {
|
||||||
#expect(issue.assignees?[1].login == "dev2")
|
#expect(issue.assignees?[1].login == "dev2")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue with nil milestone and empty assignees`() throws {
|
@Test func decodesIssueWithNilMilestoneAndEmptyAssignees() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 57,
|
"id": 57,
|
||||||
|
|
@ -168,7 +168,7 @@ struct ModelDecodingTests {
|
||||||
#expect(issue.assignees?.isEmpty == true)
|
#expect(issue.assignees?.isEmpty == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue milestone`() throws {
|
@Test func decodesIssueMilestone() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -188,7 +188,7 @@ struct ModelDecodingTests {
|
||||||
#expect(milestone.closedAt == nil)
|
#expect(milestone.closedAt == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue milestone with minimal fields`() throws {
|
@Test func decodesIssueMilestoneWithMinimalFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -204,7 +204,7 @@ struct ModelDecodingTests {
|
||||||
#expect(milestone.closedAt == nil)
|
#expect(milestone.closedAt == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue with minimal repository`() throws {
|
@Test func decodesIssueWithMinimalRepository() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 52,
|
"id": 52,
|
||||||
|
|
@ -233,7 +233,7 @@ struct ModelDecodingTests {
|
||||||
#expect(issue.repository?.defaultBranch == nil)
|
#expect(issue.repository?.defaultBranch == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue with repository`() throws {
|
@Test func decodesIssueWithRepository() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 51,
|
"id": 51,
|
||||||
|
|
@ -284,7 +284,7 @@ struct ModelDecodingTests {
|
||||||
#expect(issue.repository?.fullName == "user/my-repo")
|
#expect(issue.repository?.fullName == "user/my-repo")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue with merged pull request field`() throws {
|
@Test func decodesIssueWithMergedPullRequestField() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 53,
|
"id": 53,
|
||||||
|
|
@ -315,7 +315,7 @@ struct ModelDecodingTests {
|
||||||
#expect(issue.repository?.fullName == "org/app")
|
#expect(issue.repository?.fullName == "org/app")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue with open pull request field`() throws {
|
@Test func decodesIssueWithOpenPullRequestField() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 54,
|
"id": 54,
|
||||||
|
|
@ -346,7 +346,7 @@ struct ModelDecodingTests {
|
||||||
#expect(issue.comments == 0)
|
#expect(issue.comments == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes issue with minimal pull request field`() throws {
|
@Test func decodesIssueWithMinimalPullRequestField() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 55,
|
"id": 55,
|
||||||
|
|
@ -371,7 +371,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - PullRequest
|
// MARK: - PullRequest
|
||||||
|
|
||||||
@Test func `decodes pull request`() throws {
|
@Test func decodesPullRequest() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 200,
|
"id": 200,
|
||||||
|
|
@ -409,7 +409,7 @@ struct ModelDecodingTests {
|
||||||
#expect(pullRequest.draft == false)
|
#expect(pullRequest.draft == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes pull request with full metadata`() throws {
|
@Test func decodesPullRequestWithFullMetadata() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 204,
|
"id": 204,
|
||||||
|
|
@ -460,7 +460,7 @@ struct ModelDecodingTests {
|
||||||
#expect(pullRequest.requestedReviewers?[0].login == "reviewer1")
|
#expect(pullRequest.requestedReviewers?[0].login == "reviewer1")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes pull request with requested reviewers`() throws {
|
@Test func decodesPullRequestWithRequestedReviewers() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 202,
|
"id": 202,
|
||||||
|
|
@ -490,7 +490,7 @@ struct ModelDecodingTests {
|
||||||
#expect(pullRequest.requestedReviewers?[1].fullName == "Reviewer Two")
|
#expect(pullRequest.requestedReviewers?[1].fullName == "Reviewer Two")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes pull request without requested reviewers`() throws {
|
@Test func decodesPullRequestWithoutRequestedReviewers() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 203,
|
"id": 203,
|
||||||
|
|
@ -511,7 +511,7 @@ struct ModelDecodingTests {
|
||||||
#expect(pullRequest.requestedReviewers == nil)
|
#expect(pullRequest.requestedReviewers == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes merged pull request`() throws {
|
@Test func decodesMergedPullRequest() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 201,
|
"id": 201,
|
||||||
|
|
@ -538,7 +538,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - PullRequestReview
|
// MARK: - PullRequestReview
|
||||||
|
|
||||||
@Test func `decodes pull request review`() throws {
|
@Test func decodesPullRequestReview() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 300,
|
"id": 300,
|
||||||
|
|
@ -562,7 +562,7 @@ struct ModelDecodingTests {
|
||||||
#expect(review.official == true)
|
#expect(review.official == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes review with nullable fields`() throws {
|
@Test func decodesReviewWithNullableFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 301,
|
"id": 301,
|
||||||
|
|
@ -578,7 +578,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - ReviewComment
|
// MARK: - ReviewComment
|
||||||
|
|
||||||
@Test func `decodes review comment`() throws {
|
@Test func decodesReviewComment() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 400,
|
"id": 400,
|
||||||
|
|
@ -601,7 +601,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - CreateReviewComment encoding
|
// MARK: - CreateReviewComment encoding
|
||||||
|
|
||||||
@Test func `encodes create review comment`() throws {
|
@Test func encodesCreateReviewComment() throws {
|
||||||
let comment = CreateReviewComment(
|
let comment = CreateReviewComment(
|
||||||
body: "Fix this",
|
body: "Fix this",
|
||||||
path: "file.swift",
|
path: "file.swift",
|
||||||
|
|
@ -616,7 +616,7 @@ struct ModelDecodingTests {
|
||||||
#expect(dict["new_position"] as? Int == 7)
|
#expect(dict["new_position"] as? Int == 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `encodes create review comment with nil positions`() throws {
|
@Test func encodesCreateReviewCommentWithNilPositions() throws {
|
||||||
let comment = CreateReviewComment(
|
let comment = CreateReviewComment(
|
||||||
body: "Note",
|
body: "Note",
|
||||||
path: "readme.md",
|
path: "readme.md",
|
||||||
|
|
@ -631,7 +631,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - Repository
|
// MARK: - Repository
|
||||||
|
|
||||||
@Test func `decodes repository`() throws {
|
@Test func decodesRepository() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -677,7 +677,7 @@ struct ModelDecodingTests {
|
||||||
#expect(repo.updatedAt != nil)
|
#expect(repo.updatedAt != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes repository with minimal fields`() throws {
|
@Test func decodesRepositoryWithMinimalFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
|
|
@ -713,7 +713,7 @@ struct ModelDecodingTests {
|
||||||
#expect(repo.template == nil)
|
#expect(repo.template == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes repository with partial fields`() throws {
|
@Test func decodesRepositoryWithPartialFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 11,
|
"id": 11,
|
||||||
|
|
@ -738,7 +738,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - NotificationThread
|
// MARK: - NotificationThread
|
||||||
|
|
||||||
@Test func `decodes notification thread`() throws {
|
@Test func decodesNotificationThread() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -775,7 +775,7 @@ struct ModelDecodingTests {
|
||||||
#expect(notification.repository.fullName == "org/app")
|
#expect(notification.repository.fullName == "org/app")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes notification subject types`() throws {
|
@Test func decodesNotificationSubjectTypes() throws {
|
||||||
let issueJson = """
|
let issueJson = """
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -832,7 +832,7 @@ struct ModelDecodingTests {
|
||||||
#expect(commit.subject.state == nil)
|
#expect(commit.subject.state == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes notification count`() throws {
|
@Test func decodesNotificationCount() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{"new": 5}
|
{"new": 5}
|
||||||
"""
|
"""
|
||||||
|
|
@ -840,7 +840,7 @@ struct ModelDecodingTests {
|
||||||
#expect(count.new == 5)
|
#expect(count.new == 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes notification with minimal fields`() throws {
|
@Test func decodesNotificationWithMinimalFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
|
|
@ -867,7 +867,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - Commit
|
// MARK: - Commit
|
||||||
|
|
||||||
@Test func `decodes commit`() throws {
|
@Test func decodesCommit() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"sha": "abc123def456",
|
"sha": "abc123def456",
|
||||||
|
|
@ -913,7 +913,7 @@ struct ModelDecodingTests {
|
||||||
#expect(commit.id == "abc123def456")
|
#expect(commit.id == "abc123def456")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes commit with minimal fields`() throws {
|
@Test func decodesCommitWithMinimalFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"sha": "minimal123",
|
"sha": "minimal123",
|
||||||
|
|
@ -934,7 +934,7 @@ struct ModelDecodingTests {
|
||||||
#expect(commit.url == nil)
|
#expect(commit.url == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes commit with null author`() throws {
|
@Test func decodesCommitWithNullAuthor() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"sha": "noauthor456",
|
"sha": "noauthor456",
|
||||||
|
|
@ -959,7 +959,7 @@ struct ModelDecodingTests {
|
||||||
#expect(commit.parents?.isEmpty == true)
|
#expect(commit.parents?.isEmpty == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes commit list`() throws {
|
@Test func decodesCommitList() throws {
|
||||||
let json = """
|
let json = """
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -983,7 +983,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - RepositoryContent
|
// MARK: - RepositoryContent
|
||||||
|
|
||||||
@Test func `decodes repository content`() throws {
|
@Test func decodesRepositoryContent() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"name": "README.md",
|
"name": "README.md",
|
||||||
|
|
@ -1002,7 +1002,7 @@ struct ModelDecodingTests {
|
||||||
#expect(content.id == "README.md")
|
#expect(content.id == "README.md")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes directory content`() throws {
|
@Test func decodesDirectoryContent() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"name": "src",
|
"name": "src",
|
||||||
|
|
@ -1021,7 +1021,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - Branch
|
// MARK: - Branch
|
||||||
|
|
||||||
@Test func `decodes branch`() throws {
|
@Test func decodesBranch() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"name": "main",
|
"name": "main",
|
||||||
|
|
@ -1042,7 +1042,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - Repository Equatable / Hashable
|
// MARK: - Repository Equatable / Hashable
|
||||||
|
|
||||||
@Test func `repositories with same id are equal`() throws {
|
@Test func repositoriesWithSameIdAreEqual() throws {
|
||||||
let json1 = """
|
let json1 = """
|
||||||
{"id": 42, "name": "repo-a", "full_name": "owner/repo-a", "owner": {"id": 1, "login": "owner"}, "html_url": "https://example.com/owner/repo-a", "description": "", "empty": false, "private": false, "fork": false, "mirror": false, "archived": false, "stars_count": 0, "forks_count": 0, "open_issues_count": 0, "open_pr_counter": 0, "has_issues": true, "has_pull_requests": true, "default_branch": "main", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z"}
|
{"id": 42, "name": "repo-a", "full_name": "owner/repo-a", "owner": {"id": 1, "login": "owner"}, "html_url": "https://example.com/owner/repo-a", "description": "", "empty": false, "private": false, "fork": false, "mirror": false, "archived": false, "stars_count": 0, "forks_count": 0, "open_issues_count": 0, "open_pr_counter": 0, "has_issues": true, "has_pull_requests": true, "default_branch": "main", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z"}
|
||||||
"""
|
"""
|
||||||
|
|
@ -1055,7 +1055,7 @@ struct ModelDecodingTests {
|
||||||
#expect(repo1.hashValue == repo2.hashValue)
|
#expect(repo1.hashValue == repo2.hashValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `repositories with different ids are not equal`() throws {
|
@Test func repositoriesWithDifferentIdsAreNotEqual() throws {
|
||||||
let json1 = """
|
let json1 = """
|
||||||
{"id": 1, "name": "repo", "full_name": "owner/repo", "owner": {"id": 1, "login": "owner"}, "html_url": "https://example.com/owner/repo", "description": "", "empty": false, "private": false, "fork": false, "mirror": false, "archived": false, "stars_count": 0, "forks_count": 0, "open_issues_count": 0, "open_pr_counter": 0, "has_issues": true, "has_pull_requests": true, "default_branch": "main", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z"}
|
{"id": 1, "name": "repo", "full_name": "owner/repo", "owner": {"id": 1, "login": "owner"}, "html_url": "https://example.com/owner/repo", "description": "", "empty": false, "private": false, "fork": false, "mirror": false, "archived": false, "stars_count": 0, "forks_count": 0, "open_issues_count": 0, "open_pr_counter": 0, "has_issues": true, "has_pull_requests": true, "default_branch": "main", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z"}
|
||||||
"""
|
"""
|
||||||
|
|
@ -1069,7 +1069,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - RepositoryPermissions
|
// MARK: - RepositoryPermissions
|
||||||
|
|
||||||
@Test func `decodes repository with full permissions`() throws {
|
@Test func decodesRepositoryWithFullPermissions() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 20,
|
"id": 20,
|
||||||
|
|
@ -1088,7 +1088,7 @@ struct ModelDecodingTests {
|
||||||
#expect(repo.permissions?.pull == true)
|
#expect(repo.permissions?.pull == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes repository with read only permissions`() throws {
|
@Test func decodesRepositoryWithReadOnlyPermissions() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 21,
|
"id": 21,
|
||||||
|
|
@ -1107,7 +1107,7 @@ struct ModelDecodingTests {
|
||||||
#expect(repo.permissions?.pull == true)
|
#expect(repo.permissions?.pull == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes repository with missing permissions`() throws {
|
@Test func decodesRepositoryWithMissingPermissions() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 22,
|
"id": 22,
|
||||||
|
|
@ -1121,7 +1121,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - Token
|
// MARK: - Token
|
||||||
|
|
||||||
@Test func `decodes token`() throws {
|
@Test func decodesToken() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -1137,7 +1137,7 @@ struct ModelDecodingTests {
|
||||||
#expect(token.tokenLastEight == "56789abc")
|
#expect(token.tokenLastEight == "56789abc")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes token with minimal fields`() throws {
|
@Test func decodesTokenWithMinimalFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -1154,7 +1154,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - WorkflowRun (Forgejo's ActionRun schema)
|
// MARK: - WorkflowRun (Forgejo's ActionRun schema)
|
||||||
|
|
||||||
@Test func `decodes workflow run with full fields`() throws {
|
@Test func decodesWorkflowRunWithFullFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 100,
|
"id": 100,
|
||||||
|
|
@ -1194,7 +1194,7 @@ struct ModelDecodingTests {
|
||||||
#expect(run.durationNanos == 240_000_000_000)
|
#expect(run.durationNanos == 240_000_000_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes workflow run with minimal fields`() throws {
|
@Test func decodesWorkflowRunWithMinimalFields() throws {
|
||||||
// Minimum the Forgejo API guarantees for an in-flight run: id and status.
|
// Minimum the Forgejo API guarantees for an in-flight run: id and status.
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
|
|
@ -1212,7 +1212,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - WorkflowRunView (experimental web-route response)
|
// MARK: - WorkflowRunView (experimental web-route response)
|
||||||
|
|
||||||
@Test func `decodes workflow run view`() throws {
|
@Test func decodesWorkflowRunView() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"state": {
|
"state": {
|
||||||
|
|
@ -1257,7 +1257,7 @@ struct ModelDecodingTests {
|
||||||
#expect(view.logs.stepsLog.first?.lines.first?.message == "hello")
|
#expect(view.logs.stepsLog.first?.lines.first?.message == "hello")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes workflow run view with empty logs`() throws {
|
@Test func decodesWorkflowRunViewWithEmptyLogs() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"state": {
|
"state": {
|
||||||
|
|
@ -1284,7 +1284,7 @@ struct ModelDecodingTests {
|
||||||
|
|
||||||
// MARK: - DecodingError description
|
// MARK: - DecodingError description
|
||||||
|
|
||||||
@Test func `decoding error at root shows root`() {
|
@Test func decodingErrorAtRootShowsRoot() {
|
||||||
// Decoding a String where an object is expected produces a root-level error
|
// Decoding a String where an object is expected produces a root-level error
|
||||||
let json = #""not-an-object""#
|
let json = #""not-an-object""#
|
||||||
do {
|
do {
|
||||||
|
|
|
||||||
|
|
@ -3,47 +3,47 @@ import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
struct NormalizeServerURLTests {
|
struct NormalizeServerURLTests {
|
||||||
@Test func `strips trailing slash`() {
|
@Test func stripsTrailingSlash() {
|
||||||
let result = ForgejoClient.normalizeServerURL("https://forgejo.example.com/")
|
let result = ForgejoClient.normalizeServerURL("https://forgejo.example.com/")
|
||||||
#expect(result == "https://forgejo.example.com")
|
#expect(result == "https://forgejo.example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `adds https when no scheme`() {
|
@Test func addsHttpsWhenNoScheme() {
|
||||||
let result = ForgejoClient.normalizeServerURL("forgejo.example.com")
|
let result = ForgejoClient.normalizeServerURL("forgejo.example.com")
|
||||||
#expect(result == "https://forgejo.example.com")
|
#expect(result == "https://forgejo.example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `preserves http scheme`() {
|
@Test func preservesHttpScheme() {
|
||||||
let result = ForgejoClient.normalizeServerURL("http://localhost:3000")
|
let result = ForgejoClient.normalizeServerURL("http://localhost:3000")
|
||||||
#expect(result == "http://localhost:3000")
|
#expect(result == "http://localhost:3000")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `preserves https scheme`() {
|
@Test func preservesHttpsScheme() {
|
||||||
let result = ForgejoClient.normalizeServerURL("https://forgejo.example.com")
|
let result = ForgejoClient.normalizeServerURL("https://forgejo.example.com")
|
||||||
#expect(result == "https://forgejo.example.com")
|
#expect(result == "https://forgejo.example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `trims whitespace`() {
|
@Test func trimsWhitespace() {
|
||||||
let result = ForgejoClient.normalizeServerURL(" https://forgejo.example.com ")
|
let result = ForgejoClient.normalizeServerURL(" https://forgejo.example.com ")
|
||||||
#expect(result == "https://forgejo.example.com")
|
#expect(result == "https://forgejo.example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `handles trailing slash and whitespace`() {
|
@Test func handlesTrailingSlashAndWhitespace() {
|
||||||
let result = ForgejoClient.normalizeServerURL(" forgejo.example.com/ ")
|
let result = ForgejoClient.normalizeServerURL(" forgejo.example.com/ ")
|
||||||
#expect(result == "https://forgejo.example.com")
|
#expect(result == "https://forgejo.example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `preserves port`() {
|
@Test func preservesPort() {
|
||||||
let result = ForgejoClient.normalizeServerURL("https://forgejo.example.com:3000")
|
let result = ForgejoClient.normalizeServerURL("https://forgejo.example.com:3000")
|
||||||
#expect(result == "https://forgejo.example.com:3000")
|
#expect(result == "https://forgejo.example.com:3000")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `preserves path`() {
|
@Test func preservesPath() {
|
||||||
let result = ForgejoClient.normalizeServerURL("https://example.com/forgejo")
|
let result = ForgejoClient.normalizeServerURL("https://example.com/forgejo")
|
||||||
#expect(result == "https://example.com/forgejo")
|
#expect(result == "https://example.com/forgejo")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `strips multiple trailing slashes`() {
|
@Test func stripsMultipleTrailingSlashes() {
|
||||||
let result = ForgejoClient.normalizeServerURL("https://example.com///")
|
let result = ForgejoClient.normalizeServerURL("https://example.com///")
|
||||||
#expect(result == "https://example.com")
|
#expect(result == "https://example.com")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,61 +11,61 @@ struct NotificationTests {
|
||||||
|
|
||||||
// MARK: - Subject number extraction
|
// MARK: - Subject number extraction
|
||||||
|
|
||||||
@Test func `extracts issue number from subject url`() {
|
@Test func extractsIssueNumberFromSubjectUrl() {
|
||||||
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42"
|
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42"
|
||||||
#expect(notificationSubjectNumber(from: url) == 42)
|
#expect(notificationSubjectNumber(from: url) == 42)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `extracts pull request number from subject url`() {
|
@Test func extractsPullRequestNumberFromSubjectUrl() {
|
||||||
let url = "https://forgejo.example.com/api/v1/repos/user/repo/pulls/7"
|
let url = "https://forgejo.example.com/api/v1/repos/user/repo/pulls/7"
|
||||||
#expect(notificationSubjectNumber(from: url) == 7)
|
#expect(notificationSubjectNumber(from: url) == 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `returns nil for nil url`() {
|
@Test func returnsNilForNilUrl() {
|
||||||
#expect(notificationSubjectNumber(from: nil) == nil)
|
#expect(notificationSubjectNumber(from: nil) == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `returns nil for non numeric last component`() {
|
@Test func returnsNilForNonNumericLastComponent() {
|
||||||
let url = "https://forgejo.example.com/api/v1/repos/org/app/commits/abc123"
|
let url = "https://forgejo.example.com/api/v1/repos/org/app/commits/abc123"
|
||||||
#expect(notificationSubjectNumber(from: url) == nil)
|
#expect(notificationSubjectNumber(from: url) == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `returns nil for empty string`() {
|
@Test func returnsNilForEmptyString() {
|
||||||
#expect(notificationSubjectNumber(from: "") == nil)
|
#expect(notificationSubjectNumber(from: "") == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `returns nil for invalid url`() {
|
@Test func returnsNilForInvalidUrl() {
|
||||||
#expect(notificationSubjectNumber(from: "not a url at all %%%") == nil)
|
#expect(notificationSubjectNumber(from: "not a url at all %%%") == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `extracts number from url with trailing slash`() {
|
@Test func extractsNumberFromUrlWithTrailingSlash() {
|
||||||
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42/"
|
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42/"
|
||||||
#expect(notificationSubjectNumber(from: url) == 42)
|
#expect(notificationSubjectNumber(from: url) == 42)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `extracts large number`() {
|
@Test func extractsLargeNumber() {
|
||||||
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/99999"
|
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/99999"
|
||||||
#expect(notificationSubjectNumber(from: url) == 99999)
|
#expect(notificationSubjectNumber(from: url) == 99999)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `extracts number from url with query params`() {
|
@Test func extractsNumberFromUrlWithQueryParams() {
|
||||||
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42?foo=bar&baz=1"
|
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42?foo=bar&baz=1"
|
||||||
#expect(notificationSubjectNumber(from: url) == 42)
|
#expect(notificationSubjectNumber(from: url) == 42)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `extracts number from url with fragment`() {
|
@Test func extractsNumberFromUrlWithFragment() {
|
||||||
let url = "https://forgejo.example.com/api/v1/repos/org/app/pulls/7#section"
|
let url = "https://forgejo.example.com/api/v1/repos/org/app/pulls/7#section"
|
||||||
#expect(notificationSubjectNumber(from: url) == 7)
|
#expect(notificationSubjectNumber(from: url) == 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `extracts number from url with query and fragment`() {
|
@Test func extractsNumberFromUrlWithQueryAndFragment() {
|
||||||
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/15?ref=main#comment-1"
|
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/15?ref=main#comment-1"
|
||||||
#expect(notificationSubjectNumber(from: url) == 15)
|
#expect(notificationSubjectNumber(from: url) == 15)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Full API response decoding
|
// MARK: - Full API response decoding
|
||||||
|
|
||||||
@Test func `decodes notification array`() throws {
|
@Test func decodesNotificationArray() throws {
|
||||||
let json = """
|
let json = """
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -106,19 +106,19 @@ struct NotificationTests {
|
||||||
#expect(notifications[1].subject.type == "Pull")
|
#expect(notifications[1].subject.type == "Pull")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes empty notification array`() throws {
|
@Test func decodesEmptyNotificationArray() throws {
|
||||||
let json = "[]"
|
let json = "[]"
|
||||||
let notifications = try decoder().decode([NotificationThread].self, from: Data(json.utf8))
|
let notifications = try decoder().decode([NotificationThread].self, from: Data(json.utf8))
|
||||||
#expect(notifications.isEmpty)
|
#expect(notifications.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes notification count zero`() throws {
|
@Test func decodesNotificationCountZero() throws {
|
||||||
let json = #"{"new": 0}"#
|
let json = #"{"new": 0}"#
|
||||||
let count = try decoder().decode(NotificationCount.self, from: Data(json.utf8))
|
let count = try decoder().decode(NotificationCount.self, from: Data(json.utf8))
|
||||||
#expect(count.new == 0)
|
#expect(count.new == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `decodes notification count large value`() throws {
|
@Test func decodesNotificationCountLargeValue() throws {
|
||||||
let json = #"{"new": 9999}"#
|
let json = #"{"new": 9999}"#
|
||||||
let count = try decoder().decode(NotificationCount.self, from: Data(json.utf8))
|
let count = try decoder().decode(NotificationCount.self, from: Data(json.utf8))
|
||||||
#expect(count.new == 9999)
|
#expect(count.new == 9999)
|
||||||
|
|
@ -126,7 +126,7 @@ struct NotificationTests {
|
||||||
|
|
||||||
// MARK: - Date handling in notifications
|
// MARK: - Date handling in notifications
|
||||||
|
|
||||||
@Test func `decodes notification with fractional seconds date`() throws {
|
@Test func decodesNotificationWithFractionalSecondsDate() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -144,7 +144,7 @@ struct NotificationTests {
|
||||||
|
|
||||||
// MARK: - Extra/unknown fields are ignored
|
// MARK: - Extra/unknown fields are ignored
|
||||||
|
|
||||||
@Test func `decodes notification ignoring extra fields`() throws {
|
@Test func decodesNotificationIgnoringExtraFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
|
@ -167,7 +167,7 @@ struct NotificationTests {
|
||||||
|
|
||||||
// MARK: - Required fields validation
|
// MARK: - Required fields validation
|
||||||
|
|
||||||
@Test func `fails decoding notification missing subject`() {
|
@Test func failsDecodingNotificationMissingSubject() {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
|
|
@ -182,7 +182,7 @@ struct NotificationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `fails decoding notification missing repository`() {
|
@Test func failsDecodingNotificationMissingRepository() {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 8,
|
"id": 8,
|
||||||
|
|
@ -197,7 +197,7 @@ struct NotificationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `fails decoding notification missing id`() {
|
@Test func failsDecodingNotificationMissingId() {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"unread": true,
|
"unread": true,
|
||||||
|
|
@ -212,14 +212,14 @@ struct NotificationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `fails decoding notification count missing new field`() {
|
@Test func failsDecodingNotificationCountMissingNewField() {
|
||||||
let json = #"{}"#
|
let json = #"{}"#
|
||||||
#expect(throws: DecodingError.self) {
|
#expect(throws: DecodingError.self) {
|
||||||
_ = try decoder().decode(NotificationCount.self, from: Data(json.utf8))
|
_ = try decoder().decode(NotificationCount.self, from: Data(json.utf8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `fails decoding subject missing title`() {
|
@Test func failsDecodingSubjectMissingTitle() {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 9,
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,20 @@ struct RepositoryServiceURLTests {
|
||||||
|
|
||||||
// MARK: - fetchContents URL construction
|
// MARK: - fetchContents URL construction
|
||||||
|
|
||||||
@Test func `make URL contents without ref`() throws {
|
@Test func makeURLContentsWithoutRef() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents")
|
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/contents")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/contents")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `make URL contents with ref`() throws {
|
@Test func makeURLContentsWithRef() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let queryItems = [URLQueryItem(name: "ref", value: "develop")]
|
let queryItems = [URLQueryItem(name: "ref", value: "develop")]
|
||||||
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents", queryItems: queryItems)
|
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents", queryItems: queryItems)
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/contents?ref=develop")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/contents?ref=develop")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `make URL contents with ref and subpath`() throws {
|
@Test func makeURLContentsWithRefAndSubpath() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let queryItems = [URLQueryItem(name: "ref", value: "feature/login")]
|
let queryItems = [URLQueryItem(name: "ref", value: "feature/login")]
|
||||||
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents/src/main.py", queryItems: queryItems)
|
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents/src/main.py", queryItems: queryItems)
|
||||||
|
|
@ -31,14 +31,14 @@ struct RepositoryServiceURLTests {
|
||||||
#expect(url.path == "/api/v1/repos/owner/repo/contents/src/main.py")
|
#expect(url.path == "/api/v1/repos/owner/repo/contents/src/main.py")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `make URL file content with ref`() throws {
|
@Test func makeURLFileContentWithRef() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let queryItems = [URLQueryItem(name: "ref", value: "v1.0")]
|
let queryItems = [URLQueryItem(name: "ref", value: "v1.0")]
|
||||||
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents/README.md", queryItems: queryItems)
|
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents/README.md", queryItems: queryItems)
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/contents/README.md?ref=v1.0")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/contents/README.md?ref=v1.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `make URL with empty query items`() throws {
|
@Test func makeURLWithEmptyQueryItems() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents", queryItems: [])
|
let url = try client.makeURL(path: "/api/v1/repos/owner/repo/contents", queryItems: [])
|
||||||
#expect(url.query == nil)
|
#expect(url.query == nil)
|
||||||
|
|
@ -46,13 +46,13 @@ struct RepositoryServiceURLTests {
|
||||||
|
|
||||||
// MARK: - ref parameter nil-coalescence (same pattern used by fetchContents and fetchFileContent)
|
// MARK: - ref parameter nil-coalescence (same pattern used by fetchContents and fetchFileContent)
|
||||||
|
|
||||||
@Test func `optional ref produces empty query items`() {
|
@Test func optionalRefProducesEmptyQueryItems() {
|
||||||
let ref: String? = nil
|
let ref: String? = nil
|
||||||
let queryItems = ref.map { [URLQueryItem(name: "ref", value: $0)] } ?? []
|
let queryItems = ref.map { [URLQueryItem(name: "ref", value: $0)] } ?? []
|
||||||
#expect(queryItems.isEmpty)
|
#expect(queryItems.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `present ref produces query item`() {
|
@Test func presentRefProducesQueryItem() {
|
||||||
let ref: String? = "main"
|
let ref: String? = "main"
|
||||||
let queryItems = ref.map { [URLQueryItem(name: "ref", value: $0)] } ?? []
|
let queryItems = ref.map { [URLQueryItem(name: "ref", value: $0)] } ?? []
|
||||||
#expect(queryItems.count == 1)
|
#expect(queryItems.count == 1)
|
||||||
|
|
@ -62,7 +62,7 @@ struct RepositoryServiceURLTests {
|
||||||
|
|
||||||
// MARK: - createRepository URL
|
// MARK: - createRepository URL
|
||||||
|
|
||||||
@Test func `create repository URL`() throws {
|
@Test func createRepositoryURL() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeURL(path: "/api/v1/user/repos")
|
let url = try client.makeURL(path: "/api/v1/user/repos")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/user/repos")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/user/repos")
|
||||||
|
|
@ -70,7 +70,7 @@ struct RepositoryServiceURLTests {
|
||||||
|
|
||||||
// MARK: - editRepository URL
|
// MARK: - editRepository URL
|
||||||
|
|
||||||
@Test func `edit repository URL`() throws {
|
@Test func editRepositoryURL() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "")
|
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo")
|
||||||
|
|
@ -78,7 +78,7 @@ struct RepositoryServiceURLTests {
|
||||||
|
|
||||||
// MARK: - createLabel URL
|
// MARK: - createLabel URL
|
||||||
|
|
||||||
@Test func `create label URL`() throws {
|
@Test func createLabelURL() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/labels")
|
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/labels")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/labels")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/labels")
|
||||||
|
|
@ -86,7 +86,7 @@ struct RepositoryServiceURLTests {
|
||||||
|
|
||||||
// MARK: - createMilestone URL
|
// MARK: - createMilestone URL
|
||||||
|
|
||||||
@Test func `create milestone URL`() throws {
|
@Test func createMilestoneURL() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/milestones")
|
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/milestones")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/milestones")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/milestones")
|
||||||
|
|
@ -94,7 +94,7 @@ struct RepositoryServiceURLTests {
|
||||||
|
|
||||||
// MARK: - addCollaborator URL
|
// MARK: - addCollaborator URL
|
||||||
|
|
||||||
@Test func `add collaborator URL`() throws {
|
@Test func addCollaboratorURL() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let encoded = ForgejoClient.encodedPathSegment("testbot")
|
let encoded = ForgejoClient.encodedPathSegment("testbot")
|
||||||
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/collaborators/\(encoded)")
|
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/collaborators/\(encoded)")
|
||||||
|
|
@ -103,7 +103,7 @@ struct RepositoryServiceURLTests {
|
||||||
|
|
||||||
// MARK: - createFile URL
|
// MARK: - createFile URL
|
||||||
|
|
||||||
@Test func `create file URL`() throws {
|
@Test func createFileURL() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/contents/hello.py")
|
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/contents/hello.py")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/contents/hello.py")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/contents/hello.py")
|
||||||
|
|
@ -111,7 +111,7 @@ struct RepositoryServiceURLTests {
|
||||||
|
|
||||||
// MARK: - Payload encoding
|
// MARK: - Payload encoding
|
||||||
|
|
||||||
@Test func `create repository payload encoding`() throws {
|
@Test func createRepositoryPayloadEncoding() throws {
|
||||||
let payload: [String: Any] = [
|
let payload: [String: Any] = [
|
||||||
"name": "test-repo",
|
"name": "test-repo",
|
||||||
"description": "A test repo",
|
"description": "A test repo",
|
||||||
|
|
@ -125,7 +125,7 @@ struct RepositoryServiceURLTests {
|
||||||
#expect(dict["private"] as? Bool == false)
|
#expect(dict["private"] as? Bool == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `create file payload encoding`() throws {
|
@Test func createFilePayloadEncoding() throws {
|
||||||
let payload: [String: Any] = [
|
let payload: [String: Any] = [
|
||||||
"content": "base64content",
|
"content": "base64content",
|
||||||
"message": "Add file",
|
"message": "Add file",
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,22 @@ import Testing
|
||||||
struct URLSessionManagerTests {
|
struct URLSessionManagerTests {
|
||||||
// MARK: - trustedHost scoping
|
// MARK: - trustedHost scoping
|
||||||
|
|
||||||
@Test func `self signed without trusted host accepts any`() {
|
@Test func selfSignedWithoutTrustedHostAcceptsAny() {
|
||||||
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: nil)
|
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: nil)
|
||||||
#expect(manager.allowSelfSignedCertificates == true)
|
#expect(manager.allowSelfSignedCertificates == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `self signed with trusted host is set`() {
|
@Test func selfSignedWithTrustedHostIsSet() {
|
||||||
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: "forgejo.example.com")
|
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: "forgejo.example.com")
|
||||||
#expect(manager.allowSelfSignedCertificates == true)
|
#expect(manager.allowSelfSignedCertificates == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `self signed disabled by default`() {
|
@Test func selfSignedDisabledByDefault() {
|
||||||
let manager = URLSessionManager()
|
let manager = URLSessionManager()
|
||||||
#expect(manager.allowSelfSignedCertificates == false)
|
#expect(manager.allowSelfSignedCertificates == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `session is created with configuration`() {
|
@Test func sessionIsCreatedWithConfiguration() {
|
||||||
let manager = URLSessionManager(allowSelfSignedCertificates: false)
|
let manager = URLSessionManager(allowSelfSignedCertificates: false)
|
||||||
let session = manager.session
|
let session = manager.session
|
||||||
#expect(session.configuration.timeoutIntervalForRequest == 30)
|
#expect(session.configuration.timeoutIntervalForRequest == 30)
|
||||||
|
|
@ -29,31 +29,31 @@ struct URLSessionManagerTests {
|
||||||
|
|
||||||
// MARK: - Trust disposition logic
|
// MARK: - Trust disposition logic
|
||||||
|
|
||||||
@Test func `self signed disabled returns default handling`() {
|
@Test func selfSignedDisabledReturnsDefaultHandling() {
|
||||||
let manager = URLSessionManager(allowSelfSignedCertificates: false)
|
let manager = URLSessionManager(allowSelfSignedCertificates: false)
|
||||||
let disposition = manager.trustDisposition(for: "forgejo.example.com")
|
let disposition = manager.trustDisposition(for: "forgejo.example.com")
|
||||||
#expect(disposition == .performDefaultHandling)
|
#expect(disposition == .performDefaultHandling)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `self signed enabled with nil trusted host returns default handling`() {
|
@Test func selfSignedEnabledWithNilTrustedHostReturnsDefaultHandling() {
|
||||||
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: nil)
|
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: nil)
|
||||||
let disposition = manager.trustDisposition(for: "forgejo.example.com")
|
let disposition = manager.trustDisposition(for: "forgejo.example.com")
|
||||||
#expect(disposition == .performDefaultHandling)
|
#expect(disposition == .performDefaultHandling)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `self signed enabled with matching host returns use credential`() {
|
@Test func selfSignedEnabledWithMatchingHostReturnsUseCredential() {
|
||||||
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: "forgejo.example.com")
|
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: "forgejo.example.com")
|
||||||
let disposition = manager.trustDisposition(for: "forgejo.example.com")
|
let disposition = manager.trustDisposition(for: "forgejo.example.com")
|
||||||
#expect(disposition == .useCredential)
|
#expect(disposition == .useCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `self signed enabled with mismatched host returns default handling`() {
|
@Test func selfSignedEnabledWithMismatchedHostReturnsDefaultHandling() {
|
||||||
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: "forgejo.example.com")
|
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: "forgejo.example.com")
|
||||||
let disposition = manager.trustDisposition(for: "evil.example.com")
|
let disposition = manager.trustDisposition(for: "evil.example.com")
|
||||||
#expect(disposition == .performDefaultHandling)
|
#expect(disposition == .performDefaultHandling)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `self signed trust matches case insensitive`() {
|
@Test func selfSignedTrustMatchesCaseInsensitive() {
|
||||||
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: "Forgejo.Example.COM")
|
let manager = URLSessionManager(allowSelfSignedCertificates: true, trustedHost: "Forgejo.Example.COM")
|
||||||
let disposition = manager.trustDisposition(for: "forgejo.example.com")
|
let disposition = manager.trustDisposition(for: "forgejo.example.com")
|
||||||
#expect(disposition == .useCredential)
|
#expect(disposition == .useCredential)
|
||||||
|
|
@ -63,7 +63,7 @@ struct URLSessionManagerTests {
|
||||||
struct ForgejoClientHostMatchTests {
|
struct ForgejoClientHostMatchTests {
|
||||||
// MARK: - Auth header host matching
|
// MARK: - Auth header host matching
|
||||||
|
|
||||||
@Test func `authenticated request includes auth for matching host`() throws {
|
@Test func authenticatedRequestIncludesAuthForMatchingHost() throws {
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://forgejo.example.com",
|
serverURL: "https://forgejo.example.com",
|
||||||
username: "user",
|
username: "user",
|
||||||
|
|
@ -74,7 +74,7 @@ struct ForgejoClientHostMatchTests {
|
||||||
#expect(request.value(forHTTPHeaderField: "Authorization") != nil)
|
#expect(request.value(forHTTPHeaderField: "Authorization") != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `authenticated request omits auth for different host`() throws {
|
@Test func authenticatedRequestOmitsAuthForDifferentHost() throws {
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://forgejo.example.com",
|
serverURL: "https://forgejo.example.com",
|
||||||
username: "user",
|
username: "user",
|
||||||
|
|
@ -85,7 +85,7 @@ struct ForgejoClientHostMatchTests {
|
||||||
#expect(request.value(forHTTPHeaderField: "Authorization") == nil)
|
#expect(request.value(forHTTPHeaderField: "Authorization") == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `authenticated request matches case insensitive host`() throws {
|
@Test func authenticatedRequestMatchesCaseInsensitiveHost() throws {
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://Forgejo.Example.COM",
|
serverURL: "https://Forgejo.Example.COM",
|
||||||
username: "user",
|
username: "user",
|
||||||
|
|
@ -96,7 +96,7 @@ struct ForgejoClientHostMatchTests {
|
||||||
#expect(request.value(forHTTPHeaderField: "Authorization") != nil)
|
#expect(request.value(forHTTPHeaderField: "Authorization") != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `authenticated request sets method and body`() throws {
|
@Test func authenticatedRequestSetsMethodAndBody() throws {
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://forgejo.example.com",
|
serverURL: "https://forgejo.example.com",
|
||||||
username: "user",
|
username: "user",
|
||||||
|
|
@ -110,7 +110,7 @@ struct ForgejoClientHostMatchTests {
|
||||||
#expect(request.value(forHTTPHeaderField: "Content-Type") == "application/json")
|
#expect(request.value(forHTTPHeaderField: "Content-Type") == "application/json")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `make URL constructs valid URL`() throws {
|
@Test func makeURLConstructsValidURL() throws {
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://forgejo.example.com",
|
serverURL: "https://forgejo.example.com",
|
||||||
username: "user",
|
username: "user",
|
||||||
|
|
@ -120,7 +120,7 @@ struct ForgejoClientHostMatchTests {
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/user")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/user")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `make URL with query items`() throws {
|
@Test func makeURLWithQueryItems() throws {
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://forgejo.example.com",
|
serverURL: "https://forgejo.example.com",
|
||||||
username: "user",
|
username: "user",
|
||||||
|
|
@ -134,7 +134,7 @@ struct ForgejoClientHostMatchTests {
|
||||||
#expect(url.absoluteString.contains("page=2"))
|
#expect(url.absoluteString.contains("page=2"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `client extracts host for trusted cert`() {
|
@Test func clientExtractsHostForTrustedCert() {
|
||||||
// Verify that the client passes the right host to URLSessionManager
|
// Verify that the client passes the right host to URLSessionManager
|
||||||
let client = ForgejoClient(
|
let client = ForgejoClient(
|
||||||
serverURL: "https://my-forgejo.local:3000",
|
serverURL: "https://my-forgejo.local:3000",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
struct UserServiceTests {
|
struct UserServiceTests {
|
||||||
@Test func `create token payload encodes correct keys`() throws {
|
@Test func createTokenPayloadEncodesCorrectKeys() throws {
|
||||||
let payload: [String: Any] = [
|
let payload: [String: Any] = [
|
||||||
"name": "integration-test-token",
|
"name": "integration-test-token",
|
||||||
"scopes": ["read:user", "write:user"],
|
"scopes": ["read:user", "write:user"],
|
||||||
|
|
@ -14,13 +14,13 @@ struct UserServiceTests {
|
||||||
#expect((dict["scopes"] as? [String])?.count == 2)
|
#expect((dict["scopes"] as? [String])?.count == 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `create token URL construction`() throws {
|
@Test func createTokenURLConstruction() throws {
|
||||||
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "admin", password: "pass")
|
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "admin", password: "pass")
|
||||||
let url = try client.makeURL(path: "/api/v1/users/\(ForgejoClient.encodedPathSegment("testuser"))/tokens")
|
let url = try client.makeURL(path: "/api/v1/users/\(ForgejoClient.encodedPathSegment("testuser"))/tokens")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/users/testuser/tokens")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/users/testuser/tokens")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `create token URL encodes special characters`() throws {
|
@Test func createTokenURLEncodesSpecialCharacters() throws {
|
||||||
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "admin", password: "pass")
|
let client = ForgejoClient(serverURL: "https://forgejo.example.com", username: "admin", password: "pass")
|
||||||
let encoded = ForgejoClient.encodedPathSegment("user name")
|
let encoded = ForgejoClient.encodedPathSegment("user name")
|
||||||
let url = try client.makeURL(path: "/api/v1/users/\(encoded)/tokens")
|
let url = try client.makeURL(path: "/api/v1/users/\(encoded)/tokens")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ struct WorkflowServiceURLTests {
|
||||||
ForgejoClient(serverURL: "https://forgejo.example.com", username: "user", password: "pass")
|
ForgejoClient(serverURL: "https://forgejo.example.com", username: "user", password: "pass")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `runs list URL no filters`() throws {
|
@Test func runsListURLNoFilters() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let queryItems = [
|
let queryItems = [
|
||||||
URLQueryItem(name: "page", value: "1"),
|
URLQueryItem(name: "page", value: "1"),
|
||||||
|
|
@ -22,7 +22,7 @@ struct WorkflowServiceURLTests {
|
||||||
#expect(url.query?.contains("limit=20") == true)
|
#expect(url.query?.contains("limit=20") == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `runs list URL all filters`() throws {
|
@Test func runsListURLAllFilters() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let queryItems = [
|
let queryItems = [
|
||||||
URLQueryItem(name: "page", value: "2"),
|
URLQueryItem(name: "page", value: "2"),
|
||||||
|
|
@ -46,13 +46,13 @@ struct WorkflowServiceURLTests {
|
||||||
#expect(url.query?.contains("head_sha=abc123") == true)
|
#expect(url.query?.contains("head_sha=abc123") == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `run detail URL`() throws {
|
@Test func runDetailURL() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/actions/runs/123")
|
let url = try client.makeRepoURL(owner: "owner", repo: "repo", path: "/actions/runs/123")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/actions/runs/123")
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/owner/repo/actions/runs/123")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `owner and repo are percent encoded`() throws {
|
@Test func ownerAndRepoArePercentEncoded() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeRepoURL(owner: "my org", repo: "my repo", path: "/actions/runs")
|
let url = try client.makeRepoURL(owner: "my org", repo: "my repo", path: "/actions/runs")
|
||||||
#expect(url.absoluteString.contains("my%20org"))
|
#expect(url.absoluteString.contains("my%20org"))
|
||||||
|
|
@ -61,13 +61,13 @@ struct WorkflowServiceURLTests {
|
||||||
|
|
||||||
// MARK: - Experimental run-view URL (web routes, not /api/v1)
|
// MARK: - Experimental run-view URL (web routes, not /api/v1)
|
||||||
|
|
||||||
@Test func `run view URL is under repo web route`() throws {
|
@Test func runViewURLIsUnderRepoWebRoute() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let url = try client.makeURL(path: "/owner/repo/actions/runs/12/jobs/0")
|
let url = try client.makeURL(path: "/owner/repo/actions/runs/12/jobs/0")
|
||||||
#expect(url.absoluteString == "https://forgejo.example.com/owner/repo/actions/runs/12/jobs/0")
|
#expect(url.absoluteString == "https://forgejo.example.com/owner/repo/actions/runs/12/jobs/0")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `run view URL encodes owner and repo`() throws {
|
@Test func runViewURLEncodesOwnerAndRepo() throws {
|
||||||
let client = makeClient()
|
let client = makeClient()
|
||||||
let owner = ForgejoClient.encodedPathSegment("my org")
|
let owner = ForgejoClient.encodedPathSegment("my org")
|
||||||
let repo = ForgejoClient.encodedPathSegment("my repo")
|
let repo = ForgejoClient.encodedPathSegment("my repo")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue