import Foundation import Testing @testable import OrgSocialKit // Feed with 3 posts: heading-ID, property-ID, and a reaction post (property-ID with no body) private let feed = """ #+TITLE: Mutation Test #+NICK: tester * Posts ** 2025-01-10T09:00:00+01:00 :PROPERTIES: :LANG: en :TAGS: swift :CLIENT: org-social-ios :END: First post body. ** 2025-01-11T12:00:00+01:00 :PROPERTIES: :LANG: en :END: Second post body. ** :PROPERTIES: :ID: 2025-01-12T08:00:00+01:00 :MOOD: ❤️ :REPLY_TO: https://shom.dev/social.org#2025-01-11T10:00:00+01:00 :END: """ @Suite("PostWriter – deletePost") struct PostWriterDeleteTests { let writer = PostWriter() @Test("deletes a post identified by heading timestamp") func deleteByHeadingTimestamp() throws { let result = try writer.deletePost(timestamp: "2025-01-10T09:00:00+01:00", from: feed) #expect(!result.contains("2025-01-10T09:00:00+01:00")) #expect(!result.contains("First post body.")) #expect(result.contains("Second post body.")) } @Test("deletes a second heading-ID post leaving the others intact") func deleteSecondPost() throws { let result = try writer.deletePost(timestamp: "2025-01-11T12:00:00+01:00", from: feed) #expect(!result.contains("2025-01-11T12:00:00+01:00")) #expect(!result.contains("Second post body.")) #expect(result.contains("First post body.")) #expect(result.contains("2025-01-12T08:00:00+01:00")) } @Test("deletes a post identified by :ID: property") func deleteByPropertyID() throws { let result = try writer.deletePost(timestamp: "2025-01-12T08:00:00+01:00", from: feed) #expect(!result.contains("2025-01-12T08:00:00+01:00")) #expect(result.contains("First post body.")) #expect(result.contains("Second post body.")) } @Test("throws postNotFound for unknown timestamp") func throwsOnMissingPost() throws { #expect(throws: PostWriterError.postNotFound) { try writer.deletePost(timestamp: "1999-01-01T00:00:00+00:00", from: feed) } } @Test("deleted post leaves no triple newlines") func noTripleNewlinesAfterDelete() throws { let result = try writer.deletePost(timestamp: "2025-01-10T09:00:00+01:00", from: feed) #expect(!result.contains("\n\n\n")) } @Test("global headers are preserved after delete") func headersPreservedAfterDelete() throws { let result = try writer.deletePost(timestamp: "2025-01-11T12:00:00+01:00", from: feed) #expect(result.contains("#+TITLE: Mutation Test")) #expect(result.contains("#+NICK: tester")) #expect(result.contains("* Posts")) } @Test("deleting all posts leaves the Posts section header") func postsSectionRemainsAfterAllDeleted() throws { var content = feed content = try writer.deletePost(timestamp: "2025-01-10T09:00:00+01:00", from: content) content = try writer.deletePost(timestamp: "2025-01-11T12:00:00+01:00", from: content) content = try writer.deletePost(timestamp: "2025-01-12T08:00:00+01:00", from: content) #expect(content.contains("* Posts")) #expect(!content.contains("** ")) #expect(!content.contains(":PROPERTIES:")) } } @Suite("PostWriter – editPost") struct PostWriterEditTests { let writer = PostWriter() @Test("replaces body of a heading-ID post") func editHeadingIDPost() throws { let result = try writer.editPost( timestamp: "2025-01-10T09:00:00+01:00", newText: "Updated text.", in: feed ) #expect(result.contains("Updated text.")) #expect(!result.contains("First post body.")) #expect(result.contains("2025-01-10T09:00:00+01:00")) } @Test("replaces body of a property-ID post") func editPropertyIDPost() throws { let result = try writer.editPost( timestamp: "2025-01-12T08:00:00+01:00", newText: "Now has a body.", in: feed ) #expect(result.contains("Now has a body.")) #expect(result.contains("2025-01-12T08:00:00+01:00")) } @Test("preserves all properties after edit") func propertiesPreservedAfterEdit() throws { let result = try writer.editPost( timestamp: "2025-01-10T09:00:00+01:00", newText: "New body.", in: feed ) #expect(result.contains(":LANG: en")) #expect(result.contains(":TAGS: swift")) #expect(result.contains(":CLIENT: org-social-ios")) } @Test("other posts are untouched after edit") func otherPostsUntouched() throws { let result = try writer.editPost( timestamp: "2025-01-10T09:00:00+01:00", newText: "Changed.", in: feed ) #expect(result.contains("Second post body.")) #expect(result.contains("2025-01-11T12:00:00+01:00")) #expect(result.contains("2025-01-12T08:00:00+01:00")) } @Test("throws postNotFound for unknown timestamp") func throwsOnMissingPost() throws { #expect(throws: PostWriterError.postNotFound) { try writer.editPost(timestamp: "1999-01-01T00:00:00+00:00", newText: "x", in: feed) } } @Test("trimmed whitespace in new text") func trimmedWhitespaceInNewText() throws { let result = try writer.editPost( timestamp: "2025-01-11T12:00:00+01:00", newText: " Trimmed. \n\n", in: feed ) #expect(result.contains("Trimmed.")) #expect(!result.contains(" Trimmed.")) } @Test("re-parsing edited feed finds updated text") func reparsingFindsUpdatedText() throws { let feedURL = URL(string: "https://example.com/social.org")! let result = try writer.editPost( timestamp: "2025-01-10T09:00:00+01:00", newText: "Reparseable text.", in: feed ) let profile = OrgSocialParser().parse(result) let edited = profile.posts.first { $0.timestamp == "2025-01-10T09:00:00+01:00" } #expect(edited?.text == "Reparseable text.") _ = feedURL } @Test("re-parsing deleted feed does not contain deleted post") func reparsingAfterDelete() throws { let result = try PostWriter().deletePost(timestamp: "2025-01-10T09:00:00+01:00", from: feed) let profile = OrgSocialParser().parse(result) #expect(!profile.posts.contains { $0.timestamp == "2025-01-10T09:00:00+01:00" }) #expect(profile.posts.contains { $0.timestamp == "2025-01-11T12:00:00+01:00" }) } @Test("full edit: all editable fields are written") func fullEditAllFields() throws { let writer = PostWriter() let result = try writer.editPost( timestamp: "2025-01-10T09:00:00+01:00", in: feed, newText: "Edited body with @mention placeholder", newTimestamp: "2025-02-15T08:30:00+01:00", lang: "es", tags: "edit integration", mood: "✨", visibility: .mention ) let profile = OrgSocialParser().parse(result) // Old timestamp is gone, new one is there. #expect(!profile.posts.contains { $0.timestamp == "2025-01-10T09:00:00+01:00" }) guard let edited = profile.posts.first(where: { $0.timestamp == "2025-02-15T08:30:00+01:00" }) else { Issue.record("Edited post not found"); return } #expect(edited.text.contains("Edited body")) #expect(edited.lang == "es") #expect(edited.tags == ["edit", "integration"]) #expect(edited.mood == "✨") #expect(edited.visibility == "mention") } @Test("full edit: clearing fields removes the properties") func fullEditClearsFields() throws { let writer = PostWriter() // Start from a post that had LANG/TAGS, then clear everything. let result = try writer.editPost( timestamp: "2025-01-11T12:00:00+01:00", in: feed, newText: "Cleared", newTimestamp: nil, lang: nil, tags: nil, mood: nil, visibility: .public ) let profile = OrgSocialParser().parse(result) guard let edited = profile.posts.first(where: { $0.timestamp == "2025-01-11T12:00:00+01:00" }) else { Issue.record("Edited post not found"); return } #expect(edited.lang == nil) #expect(edited.tags.isEmpty) #expect(edited.mood == nil) #expect(edited.visibility == nil) } }