Files
2026-04-20 09:41:18 +02:00

236 lines
8.0 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Testing
@testable import bookstax
import Foundation
// MARK: - TagDTO
@Suite("TagDTO")
struct TagDTOTests {
@Test("id is composed of name and value")
func idFormat() {
let tag = TagDTO(name: "status", value: "published", order: 0)
#expect(tag.id == "status:published")
}
@Test("id with empty value still includes colon separator")
func idEmptyValue() {
let tag = TagDTO(name: "featured", value: "", order: 0)
#expect(tag.id == "featured:")
}
@Test("Two tags with same name/value have equal ids")
func duplicateIds() {
let t1 = TagDTO(name: "env", value: "prod", order: 0)
let t2 = TagDTO(name: "env", value: "prod", order: 1)
#expect(t1.id == t2.id)
}
@Test("Tags decode from JSON correctly")
func decodeFromJSON() throws {
let json = """
{"name":"topic","value":"swift","order":3}
""".data(using: .utf8)!
let tag = try JSONDecoder().decode(TagDTO.self, from: json)
#expect(tag.name == "topic")
#expect(tag.value == "swift")
#expect(tag.order == 3)
}
}
// MARK: - PageDTO
private let iso8601Formatter: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
private func pageJSON(
slug: String? = nil,
priority: Int? = nil,
draft: Bool? = nil,
tags: String? = nil,
markdown: String? = nil
) -> Data {
var fields: [String] = [
#""id":1,"book_id":10,"name":"Test Page""#,
#""created_at":"2024-01-01T00:00:00.000Z","updated_at":"2024-01-01T00:00:00.000Z""#
]
if let s = slug { fields.append("\"slug\":\"\(s)\"") }
if let p = priority { fields.append("\"priority\":\(p)") }
if let d = draft { fields.append("\"draft\":\(d)") }
if let t = tags { fields.append("\"tags\":[\(t)]") }
if let m = markdown { fields.append("\"markdown\":\"\(m)\"") }
return ("{\(fields.joined(separator: ","))}").data(using: .utf8)!
}
private func makeDecoder() -> JSONDecoder {
// Mirrors the decoder used in BookStackAPI
let decoder = JSONDecoder()
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let str = try container.decode(String.self)
if let date = formatter.date(from: str) { return date }
// Fallback without fractional seconds
let fallback = ISO8601DateFormatter()
if let date = fallback.date(from: str) { return date }
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date: \(str)")
}
return decoder
}
@Suite("PageDTO JSON Decoding")
struct PageDTOTests {
@Test("Minimal JSON (no optional fields) decodes with defaults")
func minimalDecoding() throws {
let data = pageJSON()
let page = try makeDecoder().decode(PageDTO.self, from: data)
#expect(page.id == 1)
#expect(page.bookId == 10)
#expect(page.name == "Test Page")
#expect(page.slug == "") // defaults to ""
#expect(page.priority == 0) // defaults to 0
#expect(page.draftStatus == false) // defaults to false
#expect(page.tags.isEmpty) // defaults to []
#expect(page.markdown == nil)
#expect(page.html == nil)
#expect(page.chapterId == nil)
}
@Test("slug is decoded when present")
func slugDecoded() throws {
let data = pageJSON(slug: "my-page")
let page = try makeDecoder().decode(PageDTO.self, from: data)
#expect(page.slug == "my-page")
}
@Test("priority is decoded when present")
func priorityDecoded() throws {
let data = pageJSON(priority: 5)
let page = try makeDecoder().decode(PageDTO.self, from: data)
#expect(page.priority == 5)
}
@Test("draft true decodes correctly")
func draftDecoded() throws {
let data = pageJSON(draft: true)
let page = try makeDecoder().decode(PageDTO.self, from: data)
#expect(page.draftStatus == true)
}
@Test("tags array is decoded when present")
func tagsDecoded() throws {
let tagJSON = #"{"name":"lang","value":"swift","order":0}"#
let data = pageJSON(tags: tagJSON)
let page = try makeDecoder().decode(PageDTO.self, from: data)
#expect(page.tags.count == 1)
#expect(page.tags.first?.name == "lang")
}
@Test("markdown is decoded when present")
func markdownDecoded() throws {
let data = pageJSON(markdown: "# Hello")
let page = try makeDecoder().decode(PageDTO.self, from: data)
#expect(page.markdown == "# Hello")
}
}
// MARK: - SearchResultDTO
@Suite("SearchResultDTO JSON Decoding")
struct SearchResultDTOTests {
private func resultJSON(withTags: Bool = false) -> Data {
let tags = withTags ? #","tags":[{"name":"topic","value":"swift","order":0}]"# : ""
return """
{"id":7,"name":"Swift Basics","slug":"swift-basics","type":"page",
"url":"https://example.com/books/1/page/7","preview":"An intro to Swift"\(tags)}
""".data(using: .utf8)!
}
@Test("Result with no tags field defaults to empty array")
func defaultsEmptyTags() throws {
let result = try JSONDecoder().decode(SearchResultDTO.self, from: resultJSON())
#expect(result.tags.isEmpty)
}
@Test("Result with tags decodes them correctly")
func decodesTagsWhenPresent() throws {
let result = try JSONDecoder().decode(SearchResultDTO.self, from: resultJSON(withTags: true))
#expect(result.tags.count == 1)
}
@Test("All fields decode correctly")
func allFieldsDecode() throws {
let result = try JSONDecoder().decode(SearchResultDTO.self, from: resultJSON())
#expect(result.id == 7)
#expect(result.name == "Swift Basics")
#expect(result.slug == "swift-basics")
#expect(result.type == .page)
#expect(result.preview == "An intro to Swift")
}
}
// MARK: - SearchResultDTO.ContentType
@Suite("SearchResultDTO.ContentType")
struct ContentTypeTests {
@Test("All content types have non-empty systemImage")
func systemImagesNonEmpty() {
for type_ in SearchResultDTO.ContentType.allCases {
#expect(!type_.systemImage.isEmpty)
}
}
@Test("Page system image is doc.text")
func pageSystemImage() {
#expect(SearchResultDTO.ContentType.page.systemImage == "doc.text")
}
@Test("Book system image is book.closed")
func bookSystemImage() {
#expect(SearchResultDTO.ContentType.book.systemImage == "book.closed")
}
@Test("Chapter system image is list.bullet.rectangle")
func chapterSystemImage() {
#expect(SearchResultDTO.ContentType.chapter.systemImage == "list.bullet.rectangle")
}
@Test("Shelf system image is books.vertical")
func shelfSystemImage() {
#expect(SearchResultDTO.ContentType.shelf.systemImage == "books.vertical")
}
@Test("ContentType decodes from raw string value")
func rawValueDecoding() throws {
let data = #""page""#.data(using: .utf8)!
let type_ = try JSONDecoder().decode(SearchResultDTO.ContentType.self, from: data)
#expect(type_ == .page)
}
}
// MARK: - PaginatedResponse
@Suite("PaginatedResponse Decoding")
struct PaginatedResponseTests {
@Test("Paginated shelf response decodes total and data")
func paginatedDecoding() throws {
let json = """
{"data":[{"id":1,"name":"My Shelf","slug":"my-shelf","description":"",
"created_at":"2024-01-01T00:00:00.000Z","updated_at":"2024-01-01T00:00:00.000Z"}],
"total":1}
""".data(using: .utf8)!
let decoded = try makeDecoder().decode(PaginatedResponse<ShelfDTO>.self, from: json)
#expect(decoded.total == 1)
#expect(decoded.data.count == 1)
#expect(decoded.data.first?.name == "My Shelf")
}
}