import Foundation import Testing @testable import OrgSocialKit // Known post with a live reaction on the relay (shom Sept 2025 post, 1 reaction: 🫠) private let postWithReaction = "https://shom.dev/social.org#2025-09-06T22:36:20-0500" private let relay = URL(string: "https://relay.org-social.org")! // MARK: - Unit tests (no network) @Suite("NewPostOptions – reaction factory") struct ReactionOptionsTests { @Test("reaction factory sets replyTo and mood, empty text") func reactionFactory() { let opts = NewPostOptions.reaction(to: "https://example.com/social.org#2025-01-01T00:00:00Z", mood: "❤️") #expect(opts.replyTo == "https://example.com/social.org#2025-01-01T00:00:00Z") #expect(opts.mood == "❤️") #expect(opts.text.isEmpty) } } @Suite("PostWriter – reaction block") struct ReactionBlockTests { let writer = PostWriter() @Test("reaction block has REPLY_TO and MOOD, no body text") func reactionBlockStructure() { let opts = NewPostOptions.reaction(to: "https://shom.dev/social.org#2025-09-06T22:36:20-0500", mood: "🫠") let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts) #expect(block.contains(":REPLY_TO: https://shom.dev/social.org#2025-09-06T22:36:20-0500")) #expect(block.contains(":MOOD: 🫠")) // Body text must be absent let lines = block.components(separatedBy: "\n") let bodyLines = lines.drop(while: { !$0.hasPrefix(":END:") }).dropFirst() let nonEmpty = bodyLines.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } #expect(nonEmpty.isEmpty, "Reaction block must have no body text, found: \(nonEmpty)") } @Test("reaction block has CLIENT property") func reactionBlockHasClient() { let opts = NewPostOptions.reaction(to: "https://example.com/social.org#2025-01-01T00:00:00Z", mood: "👍") let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts) #expect(block.contains(":CLIENT: iOS")) } @Test("appendPost with reaction options produces valid org content") func appendReactionPost() throws { let feed = """ #+TITLE: Test #+NICK: test * Posts """ let opts = NewPostOptions.reaction(to: "https://shom.dev/social.org#2025-09-06T22:36:20-0500", mood: "❤️") let (updated, postURL) = try writer.appendPost( to: feed, feedURL: URL(string: "https://example.com/social.org")!, options: opts ) #expect(updated.contains(":MOOD: ❤️")) #expect(updated.contains(":REPLY_TO: https://shom.dev/social.org#2025-09-06T22:36:20-0500")) #expect(postURL.hasPrefix("https://example.com/social.org#")) } } // MARK: - Integration tests (live relay) @Suite("ThreadClient – reactions", .serialized) struct ReactionIntegrationTests { let client = ThreadClient() @Test("fetchInteractions returns at least one reaction for known post") func reactionsNonEmpty() async throws { let interactions = try await client.fetchInteractions(for: postWithReaction, from: relay) #expect(interactions.reactionCount >= 1) #expect(!interactions.reactions.isEmpty) } @Test("each reaction mood has a non-empty emoji") func reactionEmojisValid() async throws { let interactions = try await client.fetchInteractions(for: postWithReaction, from: relay) for mood in interactions.reactions { #expect(!mood.emoji.isEmpty) #expect(!mood.posts.isEmpty) } } @Test("reactions are sorted by count descending") func reactionsSortedByCount() async throws { let interactions = try await client.fetchInteractions(for: postWithReaction, from: relay) let counts = interactions.reactions.map(\.posts.count) #expect(counts == counts.sorted(by: >)) } }