236 lines
8.0 KiB
Swift
236 lines
8.0 KiB
Swift
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")
|
||
}
|
||
}
|