mirror of
https://codeberg.org/secana/ForgejoKit.git
synced 2026-06-16 05:13:53 -07:00
249 lines
9.7 KiB
Swift
249 lines
9.7 KiB
Swift
@testable import ForgejoKit
|
|
import Foundation
|
|
import Testing
|
|
|
|
struct AttachmentTests {
|
|
private func decoder() -> JSONDecoder {
|
|
let dec = JSONDecoder()
|
|
dec.dateDecodingStrategy = forgejoDateDecodingStrategy
|
|
return dec
|
|
}
|
|
|
|
// MARK: - Model decoding
|
|
|
|
@Test func decodesAttachment() throws {
|
|
let json = """
|
|
{
|
|
"id": 1,
|
|
"uuid": "ee01801f-ffdb-4154-856b-77eab52aaad2",
|
|
"name": "screenshot.png",
|
|
"size": 222208,
|
|
"browser_download_url": "https://codeberg.org/attachments/ee01801f-ffdb-4154-856b-77eab52aaad2",
|
|
"type": "image/png",
|
|
"created_at": "2026-06-14T17:49:26Z"
|
|
}
|
|
"""
|
|
let attachment = try decoder().decode(Attachment.self, from: Data(json.utf8))
|
|
#expect(attachment.id == 1)
|
|
#expect(attachment.uuid == "ee01801f-ffdb-4154-856b-77eab52aaad2")
|
|
#expect(attachment.name == "screenshot.png")
|
|
#expect(attachment.size == 222_208)
|
|
#expect(attachment.browserDownloadUrl == "https://codeberg.org/attachments/ee01801f-ffdb-4154-856b-77eab52aaad2")
|
|
#expect(attachment.type == "image/png")
|
|
#expect(attachment.isImage == true)
|
|
}
|
|
|
|
@Test func decodesAttachmentWithoutTypeField() throws {
|
|
// Forgejo API does not always include a type field, isImage falls back to file extension.
|
|
let json = """
|
|
{
|
|
"id": 3,
|
|
"uuid": "no-type-uuid",
|
|
"name": "photo.jpg",
|
|
"size": 10000,
|
|
"browser_download_url": "https://codeberg.org/attachments/no-type-uuid",
|
|
"created_at": "2026-01-01T00:00:00Z"
|
|
}
|
|
"""
|
|
let attachment = try decoder().decode(Attachment.self, from: Data(json.utf8))
|
|
#expect(attachment.type == nil)
|
|
#expect(attachment.isImage == true)
|
|
}
|
|
|
|
@Test func decodesNonImageAttachment() throws {
|
|
let json = """
|
|
{
|
|
"id": 2,
|
|
"uuid": "abc123",
|
|
"name": "report.pdf",
|
|
"size": 50000,
|
|
"browser_download_url": "https://forgejo.example.com/attachments/abc123",
|
|
"type": "application/pdf",
|
|
"created_at": "2026-01-01T00:00:00Z"
|
|
}
|
|
"""
|
|
let attachment = try decoder().decode(Attachment.self, from: Data(json.utf8))
|
|
#expect(attachment.name == "report.pdf")
|
|
#expect(attachment.type == "application/pdf")
|
|
#expect(attachment.isImage == false)
|
|
}
|
|
|
|
@Test func decodesIssueWithAssets() throws {
|
|
let json = """
|
|
{
|
|
"id": 1,
|
|
"number": 42,
|
|
"title": "Bug report",
|
|
"state": "open",
|
|
"user": { "id": 1, "login": "alice" },
|
|
"labels": [],
|
|
"created_at": "2026-01-01T00:00:00Z",
|
|
"updated_at": "2026-01-01T00:00:00Z",
|
|
"comments": 0,
|
|
"assets": [
|
|
{
|
|
"id": 10,
|
|
"uuid": "uuid-1",
|
|
"name": "bug.png",
|
|
"size": 1024,
|
|
"browser_download_url": "https://forgejo.example.com/attachments/uuid-1",
|
|
"type": "image/png",
|
|
"created_at": "2026-01-01T00:00:00Z"
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
let issue = try decoder().decode(Issue.self, from: Data(json.utf8))
|
|
#expect(issue.assets?.count == 1)
|
|
#expect(issue.assets?.first?.name == "bug.png")
|
|
#expect(issue.assets?.first?.isImage == true)
|
|
}
|
|
|
|
@Test func decodesIssueWithNoAssets() throws {
|
|
let json = """
|
|
{
|
|
"id": 1,
|
|
"number": 1,
|
|
"title": "No attachments",
|
|
"state": "open",
|
|
"user": { "id": 1, "login": "alice" },
|
|
"labels": [],
|
|
"created_at": "2026-01-01T00:00:00Z",
|
|
"updated_at": "2026-01-01T00:00:00Z",
|
|
"comments": 0
|
|
}
|
|
"""
|
|
let issue = try decoder().decode(Issue.self, from: Data(json.utf8))
|
|
#expect(issue.assets == nil)
|
|
}
|
|
|
|
@Test func decodesIssueCommentWithAssets() throws {
|
|
let json = """
|
|
{
|
|
"id": 5,
|
|
"body": "See attached screenshot",
|
|
"user": { "id": 2, "login": "bob" },
|
|
"created_at": "2026-01-01T00:00:00Z",
|
|
"updated_at": "2026-01-01T00:00:00Z",
|
|
"assets": [
|
|
{
|
|
"id": 20,
|
|
"uuid": "uuid-2",
|
|
"name": "comment-image.jpg",
|
|
"size": 2048,
|
|
"browser_download_url": "https://forgejo.example.com/attachments/uuid-2",
|
|
"type": "image/jpeg",
|
|
"created_at": "2026-01-01T00:00:00Z"
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
let comment = try decoder().decode(IssueComment.self, from: Data(json.utf8))
|
|
#expect(comment.assets?.count == 1)
|
|
#expect(comment.assets?.first?.name == "comment-image.jpg")
|
|
}
|
|
|
|
// MARK: - isImage
|
|
|
|
@Test func isImageForVariousMimeTypes() {
|
|
let makeAttachment = { (type: String) in
|
|
Attachment(
|
|
id: 1, uuid: "u", name: "f.bin", size: 0,
|
|
browserDownloadUrl: "https://example.com/f",
|
|
type: type, created: Date(),
|
|
)
|
|
}
|
|
#expect(makeAttachment("image/png").isImage == true)
|
|
#expect(makeAttachment("image/jpeg").isImage == true)
|
|
#expect(makeAttachment("image/gif").isImage == true)
|
|
#expect(makeAttachment("image/webp").isImage == true)
|
|
#expect(makeAttachment("application/pdf").isImage == false)
|
|
#expect(makeAttachment("text/plain").isImage == false)
|
|
#expect(makeAttachment("application/zip").isImage == false)
|
|
}
|
|
|
|
@Test func isImageFallsBackToFileExtensionWhenTypeIsNil() {
|
|
let makeAttachment = { (name: String) in
|
|
Attachment(
|
|
id: 1, uuid: "u", name: name, size: 0,
|
|
browserDownloadUrl: "https://example.com/f",
|
|
type: nil, created: Date(),
|
|
)
|
|
}
|
|
#expect(makeAttachment("photo.png").isImage == true)
|
|
#expect(makeAttachment("photo.jpg").isImage == true)
|
|
#expect(makeAttachment("photo.jpeg").isImage == true)
|
|
#expect(makeAttachment("photo.gif").isImage == true)
|
|
#expect(makeAttachment("photo.webp").isImage == true)
|
|
#expect(makeAttachment("document.pdf").isImage == false)
|
|
#expect(makeAttachment("archive.zip").isImage == false)
|
|
#expect(makeAttachment("readme.txt").isImage == false)
|
|
}
|
|
|
|
@Test func isImageFallsBackToExtensionWhenTypeIsNotAMimeType() {
|
|
// Forgejo returns "type": "attachment" for all uploads, not an actual MIME type.
|
|
// isImage must fall through to the extension check in this case.
|
|
let makeAttachment = { (name: String) in
|
|
Attachment(
|
|
id: 1, uuid: "u", name: name, size: 0,
|
|
browserDownloadUrl: "https://example.com/f",
|
|
type: "attachment", created: Date(),
|
|
)
|
|
}
|
|
#expect(makeAttachment("photo.png").isImage == true)
|
|
#expect(makeAttachment("photo.jpg").isImage == true)
|
|
#expect(makeAttachment("photo.gif").isImage == true)
|
|
#expect(makeAttachment("document.pdf").isImage == false)
|
|
#expect(makeAttachment("archive.zip").isImage == false)
|
|
}
|
|
|
|
// MARK: - Upload URL construction
|
|
|
|
@Test func uploadIssueAttachmentURL() throws {
|
|
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")
|
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/alice/myrepo/issues/42/assets")
|
|
}
|
|
|
|
@Test func uploadCommentAttachmentURL() throws {
|
|
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")
|
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/alice/myrepo/issues/comments/99/assets")
|
|
}
|
|
|
|
@Test func uploadURLEncodesOwnerAndRepo() throws {
|
|
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")
|
|
#expect(url.absoluteString == "https://forgejo.example.com/api/v1/repos/my%20org/my%20repo/issues/1/assets")
|
|
}
|
|
|
|
// MARK: - Multipart body
|
|
|
|
@Test func buildMultipartBodyStructure() {
|
|
let fileData = Data("hello".utf8)
|
|
let boundary = "test-boundary-123"
|
|
let body = ForgejoClient.buildMultipartBody(
|
|
fileData: fileData,
|
|
fileName: "test.png",
|
|
mimeType: "image/png",
|
|
boundary: boundary,
|
|
)
|
|
let bodyString = String(data: body, encoding: .utf8) ?? ""
|
|
#expect(bodyString.contains("--\(boundary)\r\n"))
|
|
#expect(bodyString.contains("Content-Disposition: form-data; name=\"attachment\"; filename=\"test.png\""))
|
|
#expect(bodyString.contains("Content-Type: image/png"))
|
|
#expect(bodyString.contains("hello"))
|
|
#expect(bodyString.hasSuffix("--\(boundary)--\r\n"))
|
|
}
|
|
|
|
@Test func buildMultipartBodyContainsFileData() {
|
|
let fileData = Data([0xFF, 0xD8, 0xFF, 0xE0]) // JPEG header bytes
|
|
let boundary = "b"
|
|
let body = ForgejoClient.buildMultipartBody(
|
|
fileData: fileData, fileName: "img.jpg", mimeType: "image/jpeg", boundary: boundary,
|
|
)
|
|
let jpegHeader = Data([0xFF, 0xD8, 0xFF, 0xE0])
|
|
let range = body.range(of: jpegHeader)
|
|
#expect(range != nil)
|
|
}
|
|
}
|